Add latency test progress feedback
This commit is contained in:
@@ -168,6 +168,7 @@ export interface StoreType {
|
||||
failed: boolean;
|
||||
data: Podkop.OutboundGroup[];
|
||||
latencyFetchingSections: Record<string, boolean>;
|
||||
latencyProgressSections: Record<string, Podkop.LatencyActionProgress>;
|
||||
selectorSwitchingSections: Record<string, string>;
|
||||
subscriptionUpdatingSections: Record<string, boolean>;
|
||||
};
|
||||
@@ -270,6 +271,7 @@ const initialStore: StoreType = {
|
||||
loading: true,
|
||||
failed: false,
|
||||
latencyFetchingSections: {},
|
||||
latencyProgressSections: {},
|
||||
selectorSwitchingSections: {},
|
||||
subscriptionUpdatingSections: {},
|
||||
data: [],
|
||||
|
||||
@@ -85,6 +85,11 @@ describe('applyUiStateToStore', () => {
|
||||
section: 'main',
|
||||
tag: 'AUTO',
|
||||
job_id: 'latency-1',
|
||||
progress: {
|
||||
completed: 2,
|
||||
total: 5,
|
||||
failed: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
component: [
|
||||
@@ -129,6 +134,13 @@ describe('applyUiStateToStore', () => {
|
||||
expect(state.sectionsWidget.latencyFetchingSections).toEqual({
|
||||
main: true,
|
||||
});
|
||||
expect(state.sectionsWidget.latencyProgressSections).toEqual({
|
||||
main: {
|
||||
completed: 2,
|
||||
total: 5,
|
||||
failed: 1,
|
||||
},
|
||||
});
|
||||
expect(state.updatesActions.zapretInstall.loading).toBe(true);
|
||||
});
|
||||
|
||||
@@ -143,6 +155,13 @@ describe('applyUiStateToStore', () => {
|
||||
...store.get().sectionsWidget,
|
||||
subscriptionUpdatingSections: { main: true },
|
||||
latencyFetchingSections: { main: true },
|
||||
latencyProgressSections: {
|
||||
main: {
|
||||
completed: 1,
|
||||
total: 2,
|
||||
failed: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
updatesActions: {
|
||||
...store.get().updatesActions,
|
||||
@@ -158,6 +177,7 @@ describe('applyUiStateToStore', () => {
|
||||
expect(state.diagnosticsActions.enable.loading).toBe(true);
|
||||
expect(state.sectionsWidget.subscriptionUpdatingSections).toEqual({});
|
||||
expect(state.sectionsWidget.latencyFetchingSections).toEqual({});
|
||||
expect(state.sectionsWidget.latencyProgressSections).toEqual({});
|
||||
expect(state.updatesActions.zapretInstall.loading).toBe(false);
|
||||
});
|
||||
|
||||
@@ -187,6 +207,79 @@ describe('applyUiStateToStore', () => {
|
||||
expect(store.get().diagnosticsActions.restart.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves local latency progress while waiting for runtime progress', () => {
|
||||
setLocalLatencyAction('gemini', true);
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...store.get().sectionsWidget,
|
||||
latencyFetchingSections: { gemini: true },
|
||||
latencyProgressSections: {
|
||||
gemini: {
|
||||
completed: 0,
|
||||
total: 3,
|
||||
failed: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
applyUiStateToStore(createUiState());
|
||||
|
||||
expect(store.get().sectionsWidget.latencyFetchingSections).toEqual({
|
||||
gemini: true,
|
||||
});
|
||||
expect(store.get().sectionsWidget.latencyProgressSections).toEqual({
|
||||
gemini: {
|
||||
completed: 0,
|
||||
total: 3,
|
||||
failed: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves latency progress for running runtime actions without progress', () => {
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...store.get().sectionsWidget,
|
||||
latencyFetchingSections: { main: true },
|
||||
latencyProgressSections: {
|
||||
main: {
|
||||
completed: 2,
|
||||
total: 5,
|
||||
failed: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
applyUiStateToStore(
|
||||
createUiState({
|
||||
latency: [
|
||||
{
|
||||
success: true,
|
||||
running: true,
|
||||
kind: 'latency',
|
||||
latency_type: 'proxy_list',
|
||||
section: 'main',
|
||||
tag: '["one","two"]',
|
||||
job_id: 'latency-1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(store.get().sectionsWidget.latencyFetchingSections).toEqual({
|
||||
main: true,
|
||||
});
|
||||
expect(store.get().sectionsWidget.latencyProgressSections).toEqual({
|
||||
main: {
|
||||
completed: 2,
|
||||
total: 5,
|
||||
failed: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a running reload action to the restart control', () => {
|
||||
applyUiStateToStore(
|
||||
createUiState({
|
||||
|
||||
@@ -42,6 +42,29 @@ function getEmptyDiagnosticsActions(): StoreType['diagnosticsActions'] {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLatencyProgress(
|
||||
progress?: Podkop.LatencyActionProgress,
|
||||
): Podkop.LatencyActionProgress | undefined {
|
||||
const total = Math.trunc(Number(progress?.total ?? 0));
|
||||
|
||||
if (!Number.isFinite(total) || total <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const completedValue = Number(progress?.completed ?? 0);
|
||||
const failedValue = Number(progress?.failed ?? 0);
|
||||
const completed = Number.isFinite(completedValue)
|
||||
? Math.trunc(completedValue)
|
||||
: 0;
|
||||
const failed = Number.isFinite(failedValue) ? Math.trunc(failedValue) : 0;
|
||||
|
||||
return {
|
||||
completed: Math.min(Math.max(0, completed), total),
|
||||
total,
|
||||
failed: Math.max(0, failed),
|
||||
};
|
||||
}
|
||||
|
||||
function applyServiceState(uiState: Podkop.UiState) {
|
||||
const currentSystemInfo = store.get().diagnosticsSystemInfo;
|
||||
const nextSystemInfo = {
|
||||
@@ -56,8 +79,7 @@ function applyServiceState(uiState: Podkop.UiState) {
|
||||
|
||||
nextSystemInfo.sing_box_extended = uiState.capabilities.sing_box_extended;
|
||||
nextSystemInfo.sing_box_tiny = uiState.capabilities.sing_box_tiny;
|
||||
nextSystemInfo.sing_box_compressed =
|
||||
uiState.capabilities.sing_box_compressed;
|
||||
nextSystemInfo.sing_box_compressed = uiState.capabilities.sing_box_compressed;
|
||||
nextSystemInfo.sing_box_tailscale = uiState.capabilities.sing_box_tailscale;
|
||||
|
||||
store.set({
|
||||
@@ -78,8 +100,12 @@ function applyServiceState(uiState: Podkop.UiState) {
|
||||
function applyActionState(actions: UiActionMap = {}) {
|
||||
const current = store.get();
|
||||
const localOverlay = getLocalActionOverlay();
|
||||
const currentLatencyProgressSections =
|
||||
current.sectionsWidget.latencyProgressSections;
|
||||
const subscriptionUpdatingSections: Record<string, boolean> = {};
|
||||
const latencyFetchingSections: Record<string, boolean> = {};
|
||||
const latencyProgressSections: Record<string, Podkop.LatencyActionProgress> =
|
||||
{};
|
||||
const updatesActions = getEmptyUpdatesActions();
|
||||
const diagnosticsActions = getEmptyDiagnosticsActions();
|
||||
|
||||
@@ -92,6 +118,13 @@ function applyActionState(actions: UiActionMap = {}) {
|
||||
for (const state of actions.latency || []) {
|
||||
if (isRunningAction(state) && state.section) {
|
||||
latencyFetchingSections[state.section] = true;
|
||||
const progress = normalizeLatencyProgress(state.progress);
|
||||
if (progress) {
|
||||
latencyProgressSections[state.section] = progress;
|
||||
} else if (currentLatencyProgressSections[state.section]) {
|
||||
latencyProgressSections[state.section] =
|
||||
currentLatencyProgressSections[state.section];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +159,13 @@ function applyActionState(actions: UiActionMap = {}) {
|
||||
|
||||
for (const section of localOverlay.latencySections) {
|
||||
latencyFetchingSections[section] = true;
|
||||
if (
|
||||
!latencyProgressSections[section] &&
|
||||
currentLatencyProgressSections[section]
|
||||
) {
|
||||
latencyProgressSections[section] =
|
||||
currentLatencyProgressSections[section];
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of localOverlay.componentActions) {
|
||||
@@ -141,6 +181,7 @@ function applyActionState(actions: UiActionMap = {}) {
|
||||
...current.sectionsWidget,
|
||||
subscriptionUpdatingSections,
|
||||
latencyFetchingSections,
|
||||
latencyProgressSections,
|
||||
},
|
||||
updatesActions,
|
||||
diagnosticsActions,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
store,
|
||||
StoreType,
|
||||
} from '../../services';
|
||||
import { renderSections, renderWidget } from './partials';
|
||||
import { getLatencyTestLabel, renderSections, renderWidget } from './partials';
|
||||
import { fetchServicesInfo } from '../../fetchers/fetchServicesInfo';
|
||||
import { getClashApiSecret } from '../../methods/custom/getClashApiSecret';
|
||||
import { Podkop } from '../../types';
|
||||
@@ -33,6 +33,9 @@ import { isTransientRpcError } from '../../helpers/isTransientRpcError';
|
||||
import { shouldShowLoadingForRestoredAction } from '../../helpers/restoredActionLoading';
|
||||
|
||||
const SECTIONS_REFRESH_INTERVAL_MS = 10000;
|
||||
const LATENCY_TEST_BUTTON_CLASS = 'dashboard-sections-grid-item-test-latency';
|
||||
const LATENCY_TEST_BUTTON_LABEL_CLASS =
|
||||
'dashboard-sections-grid-item-test-latency__label';
|
||||
let sectionsRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let sectionsRefreshPromise: Promise<boolean> | null = null;
|
||||
let sectionsRefreshQueued = false;
|
||||
@@ -201,6 +204,7 @@ function setLatencyFetching(
|
||||
sectionName: string,
|
||||
fetching: boolean,
|
||||
local = false,
|
||||
progress?: Podkop.LatencyActionProgress,
|
||||
) {
|
||||
if (local || !fetching) {
|
||||
setLocalLatencyAction(sectionName, fetching && local);
|
||||
@@ -210,17 +214,25 @@ function setLatencyFetching(
|
||||
const latencyFetchingSections = {
|
||||
...sectionsWidget.latencyFetchingSections,
|
||||
};
|
||||
const latencyProgressSections = {
|
||||
...sectionsWidget.latencyProgressSections,
|
||||
};
|
||||
|
||||
if (fetching) {
|
||||
latencyFetchingSections[sectionName] = true;
|
||||
if (progress) {
|
||||
latencyProgressSections[sectionName] = progress;
|
||||
}
|
||||
} else {
|
||||
delete latencyFetchingSections[sectionName];
|
||||
delete latencyProgressSections[sectionName];
|
||||
}
|
||||
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...sectionsWidget,
|
||||
latencyFetchingSections,
|
||||
latencyProgressSections,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -532,6 +544,30 @@ async function handleChooseOutbound(
|
||||
}
|
||||
}
|
||||
|
||||
function getInitialLatencyProgress(
|
||||
latencyType: Podkop.LatencyActionState['latency_type'],
|
||||
tag: string,
|
||||
): Podkop.LatencyActionProgress | undefined {
|
||||
if (latencyType !== 'proxy_list') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const tags = JSON.parse(tag);
|
||||
if (!Array.isArray(tags)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const total = tags.filter(
|
||||
(item) => typeof item === 'string' && item.length > 0,
|
||||
).length;
|
||||
|
||||
return total > 0 ? { completed: 0, total, failed: 0 } : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestLatency(
|
||||
latencyType: Podkop.LatencyActionState['latency_type'],
|
||||
sectionName: string,
|
||||
@@ -542,7 +578,12 @@ async function handleTestLatency(
|
||||
return;
|
||||
}
|
||||
|
||||
setLatencyFetching(sectionName, true, true);
|
||||
setLatencyFetching(
|
||||
sectionName,
|
||||
true,
|
||||
true,
|
||||
getInitialLatencyProgress(latencyType, tag),
|
||||
);
|
||||
let jobId = '';
|
||||
let ownsJobFollow = false;
|
||||
let completed = false;
|
||||
@@ -899,6 +940,86 @@ async function handleUpdateSubscription(section: Podkop.OutboundGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
function shallowRecordEqual<T>(
|
||||
left: Record<string, T>,
|
||||
right: Record<string, T>,
|
||||
) {
|
||||
const leftKeys = Object.keys(left);
|
||||
const rightKeys = Object.keys(right);
|
||||
|
||||
if (leftKeys.length !== rightKeys.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return leftKeys.every((key) => left[key] === right[key]);
|
||||
}
|
||||
|
||||
function canUpdateLatencyProgressInline(
|
||||
prev: StoreType['sectionsWidget'],
|
||||
next: StoreType['sectionsWidget'],
|
||||
) {
|
||||
return (
|
||||
prev.loading === next.loading &&
|
||||
prev.failed === next.failed &&
|
||||
prev.data === next.data &&
|
||||
shallowRecordEqual(
|
||||
prev.latencyFetchingSections,
|
||||
next.latencyFetchingSections,
|
||||
) &&
|
||||
shallowRecordEqual(
|
||||
prev.subscriptionUpdatingSections,
|
||||
next.subscriptionUpdatingSections,
|
||||
) &&
|
||||
shallowRecordEqual(
|
||||
prev.selectorSwitchingSections,
|
||||
next.selectorSwitchingSections,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function findLatencyTestButton(container: HTMLElement, sectionName: string) {
|
||||
return Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>(
|
||||
`.${LATENCY_TEST_BUTTON_CLASS}`,
|
||||
),
|
||||
).find((button) => button.dataset.latencySection === sectionName);
|
||||
}
|
||||
|
||||
function updateLatencyProgressInline(
|
||||
sectionsWidget: StoreType['sectionsWidget'],
|
||||
) {
|
||||
const container = document.getElementById('dashboard-sections-grid');
|
||||
|
||||
if (!container) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const section of sectionsWidget.data) {
|
||||
if (!sectionsWidget.latencyFetchingSections[section.sectionName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const button = findLatencyTestButton(container, section.sectionName);
|
||||
const label = button?.querySelector<HTMLElement>(
|
||||
`.${LATENCY_TEST_BUTTON_LABEL_CLASS}`,
|
||||
);
|
||||
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = getLatencyTestLabel(
|
||||
sectionsWidget.latencyProgressSections[section.sectionName],
|
||||
);
|
||||
|
||||
if (label.textContent !== text) {
|
||||
label.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Renderer
|
||||
|
||||
async function renderSectionsWidget() {
|
||||
@@ -927,6 +1048,7 @@ async function renderSectionsWidget() {
|
||||
onShowUrlTestInfo: () => {},
|
||||
onUpdateSubscription: () => {},
|
||||
latencyFetching: false,
|
||||
latencyProgress: undefined,
|
||||
subscriptionUpdating: false,
|
||||
selectorSwitchingTag: undefined,
|
||||
});
|
||||
@@ -944,6 +1066,8 @@ async function renderSectionsWidget() {
|
||||
latencyFetching: Boolean(
|
||||
sectionsWidget.latencyFetchingSections[section.sectionName],
|
||||
),
|
||||
latencyProgress:
|
||||
sectionsWidget.latencyProgressSections[section.sectionName],
|
||||
subscriptionUpdating: Boolean(
|
||||
sectionsWidget.subscriptionUpdatingSections[section.sectionName],
|
||||
),
|
||||
@@ -1158,12 +1282,20 @@ async function renderServicesInfoWidget() {
|
||||
}
|
||||
|
||||
async function onStoreUpdate(
|
||||
_next: StoreType,
|
||||
_prev: StoreType,
|
||||
next: StoreType,
|
||||
prev: StoreType,
|
||||
diff: Partial<StoreType>,
|
||||
) {
|
||||
if (diff.sectionsWidget) {
|
||||
renderSectionsWidget();
|
||||
const inlineUpdated =
|
||||
canUpdateLatencyProgressInline(
|
||||
prev.sectionsWidget,
|
||||
next.sectionsWidget,
|
||||
) && updateLatencyProgressInline(next.sectionsWidget);
|
||||
|
||||
if (!inlineUpdated) {
|
||||
renderSectionsWidget();
|
||||
}
|
||||
}
|
||||
|
||||
if (diff.bandwidthWidget) {
|
||||
|
||||
@@ -28,6 +28,7 @@ interface IRenderSectionsProps {
|
||||
) => void;
|
||||
onUpdateSubscription: (section: Podkop.OutboundGroup) => void;
|
||||
latencyFetching: boolean;
|
||||
latencyProgress?: Podkop.LatencyActionProgress;
|
||||
subscriptionUpdating: boolean;
|
||||
selectorSwitchingTag?: string;
|
||||
}
|
||||
@@ -324,6 +325,25 @@ function renderSubscriptionUpdateAction(
|
||||
);
|
||||
}
|
||||
|
||||
export function getLatencyTestLabel(
|
||||
latencyProgress?: Podkop.LatencyActionProgress,
|
||||
) {
|
||||
const total = Math.trunc(Number(latencyProgress?.total ?? 0));
|
||||
if (!Number.isFinite(total) || total <= 0) {
|
||||
return _('Test latency');
|
||||
}
|
||||
|
||||
const completedValue = Number(latencyProgress?.completed ?? 0);
|
||||
const completed = Number.isFinite(completedValue)
|
||||
? Math.trunc(completedValue)
|
||||
: 0;
|
||||
|
||||
return `${_('Test latency')}: ${Math.min(
|
||||
Math.max(0, completed),
|
||||
total,
|
||||
)}/${total}`;
|
||||
}
|
||||
|
||||
function renderDefaultState({
|
||||
section,
|
||||
onChooseOutbound,
|
||||
@@ -332,6 +352,7 @@ function renderDefaultState({
|
||||
onTestLatency,
|
||||
onUpdateSubscription,
|
||||
latencyFetching,
|
||||
latencyProgress,
|
||||
subscriptionUpdating,
|
||||
selectorSwitchingTag,
|
||||
}: IRenderSectionsProps) {
|
||||
@@ -515,6 +536,7 @@ function renderDefaultState({
|
||||
{
|
||||
type: 'button',
|
||||
class: 'btn dashboard-sections-grid-item-test-latency',
|
||||
'data-latency-section': section.sectionName,
|
||||
disabled: latencyFetching ? true : undefined,
|
||||
click: (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -527,8 +549,23 @@ function renderDefaultState({
|
||||
},
|
||||
},
|
||||
latencyFetching
|
||||
? [renderLoaderCircleIcon24(), _('Test latency')]
|
||||
: _('Test latency'),
|
||||
? [
|
||||
renderLoaderCircleIcon24(),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
class: 'dashboard-sections-grid-item-test-latency__label',
|
||||
},
|
||||
getLatencyTestLabel(latencyProgress),
|
||||
),
|
||||
]
|
||||
: E(
|
||||
'span',
|
||||
{
|
||||
class: 'dashboard-sections-grid-item-test-latency__label',
|
||||
},
|
||||
_('Test latency'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -51,6 +51,7 @@ export function render() {
|
||||
onShowUrlTestInfo: () => {},
|
||||
onUpdateSubscription: () => {},
|
||||
latencyFetching: false,
|
||||
latencyProgress: undefined,
|
||||
subscriptionUpdating: false,
|
||||
selectorSwitchingTag: undefined,
|
||||
}),
|
||||
|
||||
@@ -155,6 +155,12 @@ export namespace Podkop {
|
||||
outbounds: Outbound[];
|
||||
}
|
||||
|
||||
export interface LatencyActionProgress {
|
||||
completed: number;
|
||||
total: number;
|
||||
failed?: number;
|
||||
}
|
||||
|
||||
interface SubscriptionTraffic {
|
||||
upload?: number;
|
||||
download?: number;
|
||||
@@ -473,6 +479,7 @@ export namespace Podkop {
|
||||
latency_type: 'group' | 'proxy' | 'proxy_list';
|
||||
section: string;
|
||||
tag: string;
|
||||
progress?: LatencyActionProgress;
|
||||
}
|
||||
|
||||
export interface UiState {
|
||||
|
||||
@@ -2049,6 +2049,18 @@ function renderSubscriptionUpdateAction(section, subscriptionUpdating, onUpdateS
|
||||
subscriptionUpdating ? [renderLoaderCircleIcon24(), _("Update subscriptions")] : _("Update subscriptions")
|
||||
);
|
||||
}
|
||||
function getLatencyTestLabel(latencyProgress) {
|
||||
const total = Math.trunc(Number(latencyProgress?.total ?? 0));
|
||||
if (!Number.isFinite(total) || total <= 0) {
|
||||
return _("Test latency");
|
||||
}
|
||||
const completedValue = Number(latencyProgress?.completed ?? 0);
|
||||
const completed = Number.isFinite(completedValue) ? Math.trunc(completedValue) : 0;
|
||||
return `${_("Test latency")}: ${Math.min(
|
||||
Math.max(0, completed),
|
||||
total
|
||||
)}/${total}`;
|
||||
}
|
||||
function renderDefaultState({
|
||||
section,
|
||||
onChooseOutbound,
|
||||
@@ -2057,6 +2069,7 @@ function renderDefaultState({
|
||||
onTestLatency,
|
||||
onUpdateSubscription,
|
||||
latencyFetching,
|
||||
latencyProgress,
|
||||
subscriptionUpdating,
|
||||
selectorSwitchingTag
|
||||
}) {
|
||||
@@ -2199,6 +2212,7 @@ function renderDefaultState({
|
||||
{
|
||||
type: "button",
|
||||
class: "btn dashboard-sections-grid-item-test-latency",
|
||||
"data-latency-section": section.sectionName,
|
||||
disabled: latencyFetching ? true : void 0,
|
||||
click: (event) => {
|
||||
event.preventDefault();
|
||||
@@ -2209,7 +2223,22 @@ function renderDefaultState({
|
||||
testLatency();
|
||||
}
|
||||
},
|
||||
latencyFetching ? [renderLoaderCircleIcon24(), _("Test latency")] : _("Test latency")
|
||||
latencyFetching ? [
|
||||
renderLoaderCircleIcon24(),
|
||||
E(
|
||||
"span",
|
||||
{
|
||||
class: "dashboard-sections-grid-item-test-latency__label"
|
||||
},
|
||||
getLatencyTestLabel(latencyProgress)
|
||||
)
|
||||
] : E(
|
||||
"span",
|
||||
{
|
||||
class: "dashboard-sections-grid-item-test-latency__label"
|
||||
},
|
||||
_("Test latency")
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -2349,6 +2378,7 @@ function render() {
|
||||
onUpdateSubscription: () => {
|
||||
},
|
||||
latencyFetching: false,
|
||||
latencyProgress: void 0,
|
||||
subscriptionUpdating: false,
|
||||
selectorSwitchingTag: void 0
|
||||
})
|
||||
@@ -4278,6 +4308,7 @@ var initialStore = {
|
||||
loading: true,
|
||||
failed: false,
|
||||
latencyFetchingSections: {},
|
||||
latencyProgressSections: {},
|
||||
selectorSwitchingSections: {},
|
||||
subscriptionUpdatingSections: {},
|
||||
data: []
|
||||
@@ -4598,6 +4629,21 @@ function getEmptyDiagnosticsActions() {
|
||||
stop: { loading: false }
|
||||
};
|
||||
}
|
||||
function normalizeLatencyProgress(progress) {
|
||||
const total = Math.trunc(Number(progress?.total ?? 0));
|
||||
if (!Number.isFinite(total) || total <= 0) {
|
||||
return void 0;
|
||||
}
|
||||
const completedValue = Number(progress?.completed ?? 0);
|
||||
const failedValue = Number(progress?.failed ?? 0);
|
||||
const completed = Number.isFinite(completedValue) ? Math.trunc(completedValue) : 0;
|
||||
const failed2 = Number.isFinite(failedValue) ? Math.trunc(failedValue) : 0;
|
||||
return {
|
||||
completed: Math.min(Math.max(0, completed), total),
|
||||
total,
|
||||
failed: Math.max(0, failed2)
|
||||
};
|
||||
}
|
||||
function applyServiceState(uiState) {
|
||||
const currentSystemInfo = store.get().diagnosticsSystemInfo;
|
||||
const nextSystemInfo = {
|
||||
@@ -4629,8 +4675,10 @@ function applyServiceState(uiState) {
|
||||
function applyActionState(actions = {}) {
|
||||
const current = store.get();
|
||||
const localOverlay = getLocalActionOverlay();
|
||||
const currentLatencyProgressSections = current.sectionsWidget.latencyProgressSections;
|
||||
const subscriptionUpdatingSections = {};
|
||||
const latencyFetchingSections = {};
|
||||
const latencyProgressSections = {};
|
||||
const updatesActions = getEmptyUpdatesActions();
|
||||
const diagnosticsActions = getEmptyDiagnosticsActions();
|
||||
for (const state of actions.subscription || []) {
|
||||
@@ -4641,6 +4689,12 @@ function applyActionState(actions = {}) {
|
||||
for (const state of actions.latency || []) {
|
||||
if (isRunningAction(state) && state.section) {
|
||||
latencyFetchingSections[state.section] = true;
|
||||
const progress = normalizeLatencyProgress(state.progress);
|
||||
if (progress) {
|
||||
latencyProgressSections[state.section] = progress;
|
||||
} else if (currentLatencyProgressSections[state.section]) {
|
||||
latencyProgressSections[state.section] = currentLatencyProgressSections[state.section];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const state of actions.component || []) {
|
||||
@@ -4669,6 +4723,9 @@ function applyActionState(actions = {}) {
|
||||
}
|
||||
for (const section of localOverlay.latencySections) {
|
||||
latencyFetchingSections[section] = true;
|
||||
if (!latencyProgressSections[section] && currentLatencyProgressSections[section]) {
|
||||
latencyProgressSections[section] = currentLatencyProgressSections[section];
|
||||
}
|
||||
}
|
||||
for (const key of localOverlay.componentActions) {
|
||||
updatesActions[key] = { loading: true };
|
||||
@@ -4680,7 +4737,8 @@ function applyActionState(actions = {}) {
|
||||
sectionsWidget: {
|
||||
...current.sectionsWidget,
|
||||
subscriptionUpdatingSections,
|
||||
latencyFetchingSections
|
||||
latencyFetchingSections,
|
||||
latencyProgressSections
|
||||
},
|
||||
updatesActions,
|
||||
diagnosticsActions
|
||||
@@ -5178,6 +5236,8 @@ function shouldShowLoadingForRestoredAction(state) {
|
||||
|
||||
// src/podkop/tabs/dashboard/initController.ts
|
||||
var SECTIONS_REFRESH_INTERVAL_MS = 1e4;
|
||||
var LATENCY_TEST_BUTTON_CLASS = "dashboard-sections-grid-item-test-latency";
|
||||
var LATENCY_TEST_BUTTON_LABEL_CLASS = "dashboard-sections-grid-item-test-latency__label";
|
||||
var sectionsRefreshTimer = null;
|
||||
var sectionsRefreshPromise = null;
|
||||
var sectionsRefreshQueued = false;
|
||||
@@ -5304,7 +5364,7 @@ function setSelectorSwitching(sectionName, tag) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function setLatencyFetching(sectionName, fetching, local = false) {
|
||||
function setLatencyFetching(sectionName, fetching, local = false, progress) {
|
||||
if (local || !fetching) {
|
||||
setLocalLatencyAction(sectionName, fetching && local);
|
||||
}
|
||||
@@ -5312,15 +5372,23 @@ function setLatencyFetching(sectionName, fetching, local = false) {
|
||||
const latencyFetchingSections = {
|
||||
...sectionsWidget.latencyFetchingSections
|
||||
};
|
||||
const latencyProgressSections = {
|
||||
...sectionsWidget.latencyProgressSections
|
||||
};
|
||||
if (fetching) {
|
||||
latencyFetchingSections[sectionName] = true;
|
||||
if (progress) {
|
||||
latencyProgressSections[sectionName] = progress;
|
||||
}
|
||||
} else {
|
||||
delete latencyFetchingSections[sectionName];
|
||||
delete latencyProgressSections[sectionName];
|
||||
}
|
||||
store.set({
|
||||
sectionsWidget: {
|
||||
...sectionsWidget,
|
||||
latencyFetchingSections
|
||||
latencyFetchingSections,
|
||||
latencyProgressSections
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5563,11 +5631,33 @@ async function handleChooseOutbound(sectionName, selector, tag) {
|
||||
setSelectorSwitching(sectionName);
|
||||
}
|
||||
}
|
||||
function getInitialLatencyProgress(latencyType, tag) {
|
||||
if (latencyType !== "proxy_list") {
|
||||
return void 0;
|
||||
}
|
||||
try {
|
||||
const tags = JSON.parse(tag);
|
||||
if (!Array.isArray(tags)) {
|
||||
return void 0;
|
||||
}
|
||||
const total = tags.filter(
|
||||
(item) => typeof item === "string" && item.length > 0
|
||||
).length;
|
||||
return total > 0 ? { completed: 0, total, failed: 0 } : void 0;
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
async function handleTestLatency(latencyType, sectionName, tag, timeout) {
|
||||
if (store.get().sectionsWidget.latencyFetchingSections[sectionName]) {
|
||||
return;
|
||||
}
|
||||
setLatencyFetching(sectionName, true, true);
|
||||
setLatencyFetching(
|
||||
sectionName,
|
||||
true,
|
||||
true,
|
||||
getInitialLatencyProgress(latencyType, tag)
|
||||
);
|
||||
let jobId = "";
|
||||
let ownsJobFollow = false;
|
||||
let completed = false;
|
||||
@@ -5849,6 +5939,58 @@ async function handleUpdateSubscription(section) {
|
||||
}
|
||||
}
|
||||
}
|
||||
function shallowRecordEqual(left, right) {
|
||||
const leftKeys = Object.keys(left);
|
||||
const rightKeys = Object.keys(right);
|
||||
if (leftKeys.length !== rightKeys.length) {
|
||||
return false;
|
||||
}
|
||||
return leftKeys.every((key) => left[key] === right[key]);
|
||||
}
|
||||
function canUpdateLatencyProgressInline(prev, next) {
|
||||
return prev.loading === next.loading && prev.failed === next.failed && prev.data === next.data && shallowRecordEqual(
|
||||
prev.latencyFetchingSections,
|
||||
next.latencyFetchingSections
|
||||
) && shallowRecordEqual(
|
||||
prev.subscriptionUpdatingSections,
|
||||
next.subscriptionUpdatingSections
|
||||
) && shallowRecordEqual(
|
||||
prev.selectorSwitchingSections,
|
||||
next.selectorSwitchingSections
|
||||
);
|
||||
}
|
||||
function findLatencyTestButton(container, sectionName) {
|
||||
return Array.from(
|
||||
container.querySelectorAll(
|
||||
`.${LATENCY_TEST_BUTTON_CLASS}`
|
||||
)
|
||||
).find((button) => button.dataset.latencySection === sectionName);
|
||||
}
|
||||
function updateLatencyProgressInline(sectionsWidget) {
|
||||
const container = document.getElementById("dashboard-sections-grid");
|
||||
if (!container) {
|
||||
return false;
|
||||
}
|
||||
for (const section of sectionsWidget.data) {
|
||||
if (!sectionsWidget.latencyFetchingSections[section.sectionName]) {
|
||||
continue;
|
||||
}
|
||||
const button = findLatencyTestButton(container, section.sectionName);
|
||||
const label = button?.querySelector(
|
||||
`.${LATENCY_TEST_BUTTON_LABEL_CLASS}`
|
||||
);
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
const text = getLatencyTestLabel(
|
||||
sectionsWidget.latencyProgressSections[section.sectionName]
|
||||
);
|
||||
if (label.textContent !== text) {
|
||||
label.textContent = text;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async function renderSectionsWidget() {
|
||||
logger.debug("[DASHBOARD]", "renderSectionsWidget");
|
||||
const sectionsWidget = store.get().sectionsWidget;
|
||||
@@ -5878,6 +6020,7 @@ async function renderSectionsWidget() {
|
||||
onUpdateSubscription: () => {
|
||||
},
|
||||
latencyFetching: false,
|
||||
latencyProgress: void 0,
|
||||
subscriptionUpdating: false,
|
||||
selectorSwitchingTag: void 0
|
||||
});
|
||||
@@ -5893,6 +6036,7 @@ async function renderSectionsWidget() {
|
||||
latencyFetching: Boolean(
|
||||
sectionsWidget.latencyFetchingSections[section.sectionName]
|
||||
),
|
||||
latencyProgress: sectionsWidget.latencyProgressSections[section.sectionName],
|
||||
subscriptionUpdating: Boolean(
|
||||
sectionsWidget.subscriptionUpdatingSections[section.sectionName]
|
||||
),
|
||||
@@ -6065,9 +6209,15 @@ async function renderServicesInfoWidget() {
|
||||
});
|
||||
container.replaceChildren(renderedWidget);
|
||||
}
|
||||
async function onStoreUpdate(_next, _prev, diff) {
|
||||
async function onStoreUpdate(next, prev, diff) {
|
||||
if (diff.sectionsWidget) {
|
||||
renderSectionsWidget();
|
||||
const inlineUpdated = canUpdateLatencyProgressInline(
|
||||
prev.sectionsWidget,
|
||||
next.sectionsWidget
|
||||
) && updateLatencyProgressInline(next.sectionsWidget);
|
||||
if (!inlineUpdated) {
|
||||
renderSectionsWidget();
|
||||
}
|
||||
}
|
||||
if (diff.bandwidthWidget) {
|
||||
renderBandwidthWidget();
|
||||
|
||||
@@ -156,7 +156,7 @@ function command_spec(command) {
|
||||
check_zapret2_runtime: [ "diagnostics/runtime.uc", "check-zapret2-runtime", 0 ],
|
||||
check_byedpi_runtime: [ "diagnostics/runtime.uc", "check-byedpi-runtime", 0 ],
|
||||
neutralize_zapret_defaults: [ "diagnostics/runtime.uc", "neutralize-zapret-defaults", 0 ],
|
||||
clash_api: [ "diagnostics/runtime.uc", "clash-api", 3 ],
|
||||
clash_api: [ "diagnostics/runtime.uc", "clash-api", 4 ],
|
||||
show_config: [ "diagnostics/runtime.uc", "show-config", 1 ],
|
||||
show_version: [ "diagnostics/runtime.uc", "show-version", 0 ],
|
||||
show_sing_box_config: [ "diagnostics/runtime.uc", "show-sing-box-config", 1 ],
|
||||
|
||||
@@ -1667,7 +1667,7 @@ function latency_test_url() {
|
||||
return value == "" ? DEFAULT_LATENCY_TEST_URL : value;
|
||||
}
|
||||
|
||||
function clash_api(action, arg1, arg2) {
|
||||
function clash_api(action, arg1, arg2, arg3) {
|
||||
let base_url = clash_api_url();
|
||||
let test_url = latency_test_url();
|
||||
let auth = clash_auth_args();
|
||||
@@ -1704,13 +1704,22 @@ function clash_api(action, arg1, arg2) {
|
||||
let tags = status_capture([ "clash-proxy-tags-lines", arg1 ], null);
|
||||
if (tags.status != 0)
|
||||
return clash_json_error("proxy_tags_json must be a JSON array of non-empty strings");
|
||||
let count = 0;
|
||||
let failed = 0;
|
||||
let proxy_types = clash_proxy_type_map(base_url, auth);
|
||||
let proxy_tags = [];
|
||||
for (let proxy_tag in split(tags.output, "\n")) {
|
||||
proxy_tag = as_string(proxy_tag);
|
||||
if (proxy_tag == "")
|
||||
continue;
|
||||
if (proxy_tag != "")
|
||||
push(proxy_tags, proxy_tag);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
let failed = 0;
|
||||
let progress_path = as_string(arg3);
|
||||
let total = length(proxy_tags);
|
||||
if (progress_path != "")
|
||||
module_success(SERVICE_UI_UC, [ "latency-progress-state", progress_path, count, total, failed ]);
|
||||
|
||||
let proxy_types = clash_proxy_type_map(base_url, auth);
|
||||
for (let proxy_tag in proxy_tags) {
|
||||
let args = [ "curl", "-G", "-s", clash_latency_endpoint(base_url, proxy_tag, proxy_types[proxy_tag]) ];
|
||||
for (let item in auth) push(args, item);
|
||||
push(args, "--data-urlencode");
|
||||
@@ -1718,8 +1727,10 @@ function clash_api(action, arg1, arg2) {
|
||||
push(args, "--data-urlencode");
|
||||
push(args, "timeout=" + as_string(arg2 || "5000"));
|
||||
if (status_capture([ "stdin-json" ], command_output(command_from_args(args))).status != 0)
|
||||
failed = 1;
|
||||
failed++;
|
||||
count++;
|
||||
if (progress_path != "")
|
||||
module_success(SERVICE_UI_UC, [ "latency-progress-state", progress_path, count, total, failed ]);
|
||||
}
|
||||
let result = status_capture([ "clash-proxy-latencies-result", count, failed ], null);
|
||||
if (result.output != "")
|
||||
@@ -1953,7 +1964,7 @@ else if (mode == "check-byedpi-runtime")
|
||||
else if (mode == "neutralize-zapret-defaults")
|
||||
exit(neutralize_zapret_defaults());
|
||||
else if (mode == "clash-api")
|
||||
exit(clash_api(ARGV[1], ARGV[2], ARGV[3]));
|
||||
exit(clash_api(ARGV[1], ARGV[2], ARGV[3], ARGV[4]));
|
||||
else if (mode == "show-config")
|
||||
exit(show_config(ARGV[1] || "masked"));
|
||||
else if (mode == "show-version")
|
||||
|
||||
@@ -55,6 +55,15 @@ function read_json_file(path) {
|
||||
}
|
||||
}
|
||||
|
||||
function parse_json_or_null(value) {
|
||||
try {
|
||||
return json(as_string(value));
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function write_json(value) {
|
||||
print(sprintf("%J", value), "\n");
|
||||
}
|
||||
@@ -184,6 +193,11 @@ function arg_number(value) {
|
||||
return int(value);
|
||||
}
|
||||
|
||||
function non_negative_number(value) {
|
||||
let number = arg_number(value);
|
||||
return number < 0 ? 0 : number;
|
||||
}
|
||||
|
||||
function unsigned_number(value) {
|
||||
value = as_string(value);
|
||||
if (value == "" || match(value, /[^0-9]/) != null)
|
||||
@@ -532,8 +546,42 @@ function running_service_action_value(action, source, started_at) {
|
||||
};
|
||||
}
|
||||
|
||||
function running_latency_action_value(latency_type, section, tag, started_at) {
|
||||
function latency_progress_value(completed, total, failed) {
|
||||
let progress_total = non_negative_number(total);
|
||||
let progress_completed = non_negative_number(completed);
|
||||
if (progress_total > 0 && progress_completed > progress_total)
|
||||
progress_completed = progress_total;
|
||||
|
||||
return {
|
||||
completed: progress_completed,
|
||||
total: progress_total,
|
||||
failed: non_negative_number(failed)
|
||||
};
|
||||
}
|
||||
|
||||
function proxy_list_total(tag) {
|
||||
let value = parse_json_or_null(tag);
|
||||
if (type(value) != "array")
|
||||
return 0;
|
||||
|
||||
let total = 0;
|
||||
for (let proxy_tag in value) {
|
||||
if (type(proxy_tag) == "string" && proxy_tag != "")
|
||||
total++;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
function initial_latency_progress(latency_type, tag) {
|
||||
if (as_string(latency_type) != "proxy_list")
|
||||
return null;
|
||||
|
||||
return latency_progress_value(0, proxy_list_total(tag), 0);
|
||||
}
|
||||
|
||||
function running_latency_action_value(latency_type, section, tag, started_at) {
|
||||
let value = {
|
||||
success: true,
|
||||
running: true,
|
||||
kind: "latency",
|
||||
@@ -546,6 +594,39 @@ function running_latency_action_value(latency_type, section, tag, started_at) {
|
||||
updated_at: null,
|
||||
exit_code: null
|
||||
};
|
||||
|
||||
let progress = initial_latency_progress(latency_type, tag);
|
||||
if (type(progress) == "object")
|
||||
value.progress = progress;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function latency_action_path_allowed(path) {
|
||||
path = as_string(path);
|
||||
let prefix = LATENCY_ACTION_DIR + "/";
|
||||
let suffix = ".json";
|
||||
return path != "" &&
|
||||
substr(path, 0, length(prefix)) == prefix &&
|
||||
length(path) > length(prefix) + length(suffix) &&
|
||||
substr(path, length(path) - length(suffix)) == suffix;
|
||||
}
|
||||
|
||||
function update_latency_progress_state(path, completed, total, failed) {
|
||||
if (!latency_action_path_allowed(path))
|
||||
return false;
|
||||
|
||||
let value = read_json_file(path);
|
||||
if (type(value) != "object" || value.kind != "latency" || value.running !== true)
|
||||
return false;
|
||||
|
||||
value.progress = latency_progress_value(completed, total, failed);
|
||||
value.updated_at = now_seconds();
|
||||
return write_state_file(path, value);
|
||||
}
|
||||
|
||||
function update_latency_progress_state_mode(path, completed, total, failed) {
|
||||
exit(update_latency_progress_state(path, completed, total, failed) ? 0 : 1);
|
||||
}
|
||||
|
||||
function finished_action_state_value(path, success, message, exit_code, updated_at) {
|
||||
@@ -1170,7 +1251,7 @@ function latency_clash_method(latency_type) {
|
||||
|
||||
function latency_worker(path, latency_type, tag, timeout) {
|
||||
let method = latency_clash_method(latency_type).method;
|
||||
let status = command_status(command_from_args([ BIN_PATH, "clash_api", method, tag, timeout ]) + " >/dev/null 2>&1");
|
||||
let status = command_status(command_from_args([ BIN_PATH, "clash_api", method, tag, timeout, path ]) + " >/dev/null 2>&1");
|
||||
if (status == 0)
|
||||
write_finished_action_state(path, true, "Latency test completed", status);
|
||||
else
|
||||
@@ -1318,6 +1399,8 @@ else if (mode == "service-action-update-pid")
|
||||
update_service_action_pid_mode(ARGV[1], ARGV[2]);
|
||||
else if (mode == "service-action-finish")
|
||||
finish_service_action_mode(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||||
else if (mode == "latency-progress-state")
|
||||
update_latency_progress_state_mode(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||||
else if (mode == "service-action-finish-after-command")
|
||||
finish_service_action_after_command_mode(ARGV[1], ARGV[2], ARGV[3]);
|
||||
else if (mode == "service-action-wait-worker")
|
||||
|
||||
@@ -102,6 +102,26 @@ PATH="$fake_bin:$PATH" \
|
||||
grep -Fq "url=https://latency.example/generate_204" "$WORK_DIR/fake-curl.log" ||
|
||||
fail "clash-api latency check must use settings.latency_test_url"
|
||||
|
||||
latency_action_dir="$WORK_DIR/ui-state/latency-actions"
|
||||
mkdir -p "$latency_action_dir"
|
||||
latency_state="$latency_action_dir/latency-1.json"
|
||||
printf '%s\n' '{"success":true,"running":true,"kind":"latency","latency_type":"proxy_list","section":"main","tag":"[]","started_at":100}' >"$latency_state"
|
||||
FAKE_CURL_LOG="$WORK_DIR/fake-curl-latencies.log" \
|
||||
PODKOP_UCI_STATE_FILE="$uci_state" \
|
||||
PODKOP_LIB="$PODKOP_LIB" \
|
||||
PODKOP_UI_LATENCY_ACTION_DIR="$latency_action_dir" \
|
||||
PATH="$fake_bin:$PATH" \
|
||||
ucode -L "$PODKOP_LIB" "$DIAGNOSTICS_RUNTIME" clash-api get_proxy_latencies '["proxy-a","proxy-b"]' 5000 "$latency_state" >/dev/null ||
|
||||
fail "clash-api get_proxy_latencies should update latency progress"
|
||||
JOB_STATE="$latency_state" node - <<'NODE'
|
||||
const fs = require("fs");
|
||||
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
|
||||
if (!value.progress || value.progress.completed !== 2 || value.progress.total !== 2 || value.progress.failed !== 0) {
|
||||
console.error("latency progress after proxy list mismatch");
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
firewall_rules="$(cat <<'EOF'
|
||||
firewall.@rule[0]=rule
|
||||
firewall.@rule[0].enabled='1'
|
||||
|
||||
@@ -67,6 +67,7 @@ for mode in \
|
||||
service-action-begin-if-idle \
|
||||
service-action-update-pid \
|
||||
service-action-finish-after-command \
|
||||
latency-progress-state \
|
||||
service-action-async \
|
||||
service-action-status \
|
||||
latency-test-async \
|
||||
@@ -107,6 +108,22 @@ if ui_ucode latency-type-valid invalid >/dev/null 2>&1; then
|
||||
fail "invalid latency type should be rejected"
|
||||
fi
|
||||
|
||||
latency_action_dir="$WORK_DIR/latency-actions"
|
||||
mkdir -p "$latency_action_dir"
|
||||
latency_state="$latency_action_dir/latency-1.json"
|
||||
printf '%s\n' '{"success":true,"running":true,"kind":"latency","latency_type":"proxy_list","section":"main","tag":"[]","started_at":100}' >"$latency_state"
|
||||
PODKOP_UI_LATENCY_ACTION_DIR="$latency_action_dir" \
|
||||
ui_ucode latency-progress-state "$latency_state" 2 5 1 >/dev/null ||
|
||||
fail "latency-progress-state should update running latency jobs"
|
||||
JOB_STATE="$latency_state" node - <<'NODE'
|
||||
const fs = require("fs");
|
||||
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
|
||||
if (!value.progress || value.progress.completed !== 2 || value.progress.total !== 5 || value.progress.failed !== 1) {
|
||||
console.error("latency progress state mismatch");
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
assert_eq "running & enabled" \
|
||||
"$(ui_ucode service-status-text 1 1)" \
|
||||
"running enabled status"
|
||||
@@ -182,6 +199,24 @@ export PODKOP_UI_LATENCY_ACTION_DIR="$PODKOP_UI_STATE_DIR/latency-actions"
|
||||
export PODKOP_UI_COMPONENT_ACTION_DIR="$PODKOP_UI_STATE_DIR/component-actions"
|
||||
export PODKOP_UI_SUBSCRIPTION_ACTION_DIR="$PODKOP_UI_STATE_DIR/subscription-actions"
|
||||
|
||||
latency_start="$(
|
||||
PODKOP_BIN=/bin/true \
|
||||
PODKOP_LIB="$PODKOP_FILES/usr/lib" \
|
||||
ui_ucode latency-test-async proxy_list main '["proxy-a","proxy-b"]' 5000
|
||||
)"
|
||||
latency_job_id="$(printf '%s' "$latency_start" | sed -n 's/.*"job_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||
[ -n "$latency_job_id" ] || fail "latency-test-async should return a job id"
|
||||
latency_async_state="$PODKOP_UI_LATENCY_ACTION_DIR/$latency_job_id.json"
|
||||
[ -f "$latency_async_state" ] || fail "latency-test-async should create a state file"
|
||||
JOB_STATE="$latency_async_state" node - <<'NODE'
|
||||
const fs = require("fs");
|
||||
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
|
||||
if (!value.progress || value.progress.completed !== 0 || value.progress.total !== 2 || value.progress.failed !== 0) {
|
||||
console.error("initial latency progress state mismatch");
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
job_id="$(ui_ucode service-action-begin-if-idle reload test)"
|
||||
[ -n "$job_id" ] || fail "service-action-begin-if-idle should create a job"
|
||||
service_state="$PODKOP_UI_SERVICE_ACTION_DIR/$job_id.json"
|
||||
|
||||
Reference in New Issue
Block a user