Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(desktop): fix the validation of subscribing to multiple topics #1611

Merged
merged 2 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 39 additions & 47 deletions src/components/SubscriptionsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,7 @@ import time from '@/utils/time'
import { getSubscriptionId } from '@/utils/idGenerator'
import getContextmenuPosition from '@/utils/getContextmenuPosition'
import { LeftValues } from '@/utils/styles'

enum SubscribeErrorReason {
normal,
qosSubFailed, // qos is abnormal
qosSubSysFailed, // qos is abnormal becauseof $SYS subscribe
emptySubFailed, // subscription returns empty array
}
import getErrorReason from '@/utils/mqttErrorReason'

@Component({
components: {
Expand Down Expand Up @@ -354,28 +348,6 @@ export default class SubscriptionsList extends Vue {
})
}

/**
* Get the error reason message corresponding to the enumeration.
* Check that errorReason not equal `SubscribeErrorReason.normal` before using.
* @return Return the message of failure subscribe
* @param errorReason - Type:enum, The reason cause the failed subscription
*/
private getErrorReasonMsg(errorReason: SubscribeErrorReason): VueI18n.TranslateResult {
if (errorReason === SubscribeErrorReason.normal) return ''
switch (errorReason) {
case errorReason & SubscribeErrorReason.qosSubFailed: {
return this.$t('connections.qosSubFailed')
}
case errorReason & SubscribeErrorReason.qosSubSysFailed: {
return this.$t('connections.qosSubSysFailed')
}
case errorReason & SubscribeErrorReason.emptySubFailed: {
return this.$t('connections.emptySubFailed')
}
}
return this.$t('connections.unknowSubFailed')
}

public async resubscribe() {
this.getCurrentConnection(this.connectionId)
for (let sub of this.subsList) {
Expand Down Expand Up @@ -410,6 +382,23 @@ export default class SubscriptionsList extends Vue {
}
}

private handleSubError(topic: string, qos: number) {
const isAclSubFailed = (qos: number) => {
return [128, 135].includes(qos)
}

const aclSubFailed = isAclSubFailed(qos)
const errorReasonMsg = aclSubFailed ? `. ${this.$t('connections.aclSubFailed')}` : ''
const errorReason = getErrorReason(this.record.mqttVersion as '3.1' | '3.1.1' | '5.0', qos)
const errorMsg: string = this.$t('connections.subFailed', [topic, errorReason, qos]) + errorReasonMsg
DM1-1 marked this conversation as resolved.
Show resolved Hide resolved
this.$emit('onSubError', errorMsg, `Topics: ${topic}`)
this.$log.error(
`Failed to subscribe: ${topic}, Error: ${errorReason} (Code: ${qos})${
aclSubFailed ? '. Make sure the permissions are correct, and check MQTT broker ACL configuration' : ''
}`,
)
}

public async subscribe(
{ topic, alias, qos, nl, rap, rh, subscriptionIdentifier, disabled, id }: SubscriptionModel,
isAuto?: boolean,
Expand All @@ -428,8 +417,9 @@ export default class SubscriptionsList extends Vue {
})
}
let isFinished = false

if (this.client.subscribe) {
const topicsArr = this.multiTopics ? topic.split(',') : topic
const topicsArr = this.multiTopics ? [...new Set(topic.split(','))].filter(Boolean) : topic
const aliasArr = this.multiTopics ? alias?.split(',') : alias
let properties: { subscriptionIdentifier: number } | undefined = undefined
if (this.record.mqttVersion === '5.0' && subscriptionIdentifier) {
Expand All @@ -448,21 +438,21 @@ export default class SubscriptionsList extends Vue {
this.$log.error(`Error subscribing to topic: ${error}`)
return false
}
let errorReason = SubscribeErrorReason.normal

if (!granted || (Array.isArray(granted) && granted.length < 1)) {
const successSubscriptions: string[] = []
if (!granted) {
this.$log.error('Error subscribing to topic: granted empty')
} else if (![0, 1, 2].includes(granted[0].qos) && topic.match(/^(\$SYS)/i)) {
errorReason = SubscribeErrorReason.qosSubSysFailed
} else if (![0, 1, 2].includes(granted[0].qos)) {
errorReason = SubscribeErrorReason.qosSubFailed
} else {
granted.forEach((grant) => {
if ([0, 1, 2].includes(grant.qos)) {
successSubscriptions.push(grant.topic)
} else {
setTimeout(() => {
this.handleSubError(grant.topic, grant.qos)
}, 0)
}
})
}

if (errorReason !== SubscribeErrorReason.normal) {
const errorReasonMsg: VueI18n.TranslateResult = this.getErrorReasonMsg(errorReason)
const errorMsg: string = `${this.$t('connections.subFailed')} ${errorReasonMsg}`
this.$log.error(`Error subscribing to topic: ${errorReasonMsg}`)
this.$emit('onSubError', errorMsg, `Topics: ${JSON.stringify(topicsArr)}`)
if (!successSubscriptions.length) {
return false
}
if (enable) {
Expand All @@ -473,23 +463,25 @@ export default class SubscriptionsList extends Vue {
this.saveTopicToSubList(topic, qos, undefined, undefined, id)
} else {
topicsArr.forEach((topic, index) => {
this.saveTopicToSubList(topic, qos, index, aliasArr as string[], id)
if (successSubscriptions.includes(topic)) {
this.saveTopicToSubList(topic, qos, index, aliasArr as string[], id)
}
})
}
this.$log.info(`Saved topic: ${topic}`)
this.$log.info(`Saved topic: ${successSubscriptions}`)
}
this.record.subscriptions = this.subsList
if (this.record.id) {
const { subscriptionService } = useServices()
await subscriptionService.updateSubscriptions(this.record.id, this.record.subscriptions)
this.changeSubs({ id: this.connectionId, subscriptions: this.subsList })
this.showDialog = false
let subLog = `Successfully subscribed to topic: ${topic}`
this.$log.info(subLog)
successSubscriptions.length && this.$log.info(`Successfully subscribed to topic: ${successSubscriptions}`)
}
isFinished = true
})
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
// TODO: maybe we should replace mqtt.js to mqtt-async.js
await new Promise(async (resolve) => {
Expand Down
45 changes: 12 additions & 33 deletions src/lang/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,39 +119,18 @@ export default {
hu: 'Előfizetés feliratkozás',
},
subFailed: {
zh: '订阅失败',
en: 'Subscribe Failed',
tr: 'Abone Başarısız',
ja: 'サブスクリプションが失敗しました',
hu: 'Sikertelen feliratkozás',
},
qosSubSysFailed: {
zh: '拒绝了 $SYS 主题,错误的 QoS,MQTT Broker 拒绝了订阅。请检查 ACL 配置',
en: 'Rejected the $SYS topic,Unexpected QoS, MQTT Broker declined the subscription. Please check ACL configuration',
tr: '$SYS konusu, Beklenmeyen QoS, MQTT Broker aboneliği reddetti. Lütfen ACL yapılandırmasını kontrol edin',
ja: '$SYSトピックを拒否しま。した予期しないQoS、MQTT Brokerはサブスクリプションを拒否しました。ACL構成を確認してください',
hu: '$SYS téma visszautasítva, váratlan QoS, az MQTT bróker elutasította a kapcsolatot. Kérlek ellenőrizd az ACL konfigurációt.',
},
qosSubFailed: {
zh: '错误的 QoS, SubACK 失败, 请检查 MQTT broker ACL 设置',
en: 'Unexpected QoS, SubACK failed, Please check MQTT broker ACL configuration',
tr: 'Beklenmeyen QoS, SubACK başarısız, MQTT Broker ACL yapılandırmasını kontrol edin',
ja: '予期しないQoS, SubACK失敗、MQTT Broker ACL構成を確認してください',
hu: 'Váratlan QoS, SubACK sikertelen, kérlek ellenőrizd az MQTT broker ACL konfigurációt',
},
emptySubFailed: {
zh: '订阅为空',
en: 'Subscription is empty',
tr: 'Abonelik boş',
ja: 'サブスクリプションは空です',
hu: 'Feliratkozás üres.',
},
unknowSubFailed: {
zh: '未知的订阅错误',
en: 'Unknown subscription error',
tr: 'Bilinmeyen abonelik hatası',
ja: '不明なサブスクリプションエラー',
hu: 'Ismeretlen feliratkozási hiba',
zh: '无法订阅 {0}, Error: {1} (Code: {2})',
en: 'Failed to Subscribe {0}, Error: {1}(Code: {2})',
tr: '{0} abone ollamadı, Hata: {1}(Kod: {2})',
ja: 'サブスクリプション失敗:{0}, エラー:{1} (コード:{2})',
hu: 'Előfizetés sikertelen: {0}, Hiba: {1} (Kód: {2})',
},
aclSubFailed: {
zh: '请确保权限正确,并检查 MQTT Broker 的 ACL 配置',
en: 'Make sure the permissions are correct, and check MQTT broker ACL configuration',
tr: 'İzinleri doğru olduğundan emin olun ve MQTT aracının ACL yapılandırmasını kontrol edin',
ja: '権限が正しいことを確認し、MQTTブローカーのACL設定を確認してください',
hu: 'Bizonyosodjon meg a jogosultságok helyességéről, és ellenőrizze az MQTT broker ACL konfigurációját',
},
connected: {
zh: '已连接',
Expand Down
59 changes: 59 additions & 0 deletions src/utils/mqttErrorReason.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const MqttErrorReason: Record<string, { [code: number]: string }> = {
'3.1': {
128: 'Not authorized',
},
'3.1.1': {
128: 'Not authorized',
},
'5.0': {
4: 'Disconnect with Will Message',
16: 'No matching subscribers',
17: 'No subscription existed',
24: 'Continue authentication',
25: 'Re-authenticate',
128: 'Unspecified error',
129: 'Malformed Packet',
130: 'Protocol Error',
131: 'Implementation specific error',
132: 'Unsupported Protocol Version',
133: 'Client Identifier not valid',
134: 'Bad User Name or Password',
135: 'Not authorized',
136: 'Server unavailable',
137: 'Server busy',
138: 'Banned',
139: 'Server shutting down',
140: 'Bad authentication method',
141: 'Keep Alive timeout',
142: 'Session taken over',
143: 'Topic Filter invalid',
144: 'Topic Name invalid',
145: 'Packet Identifier in use',
146: 'Packet Identifier not found',
147: 'Receive Maximum exceeded',
148: 'Topic Alias invalid',
149: 'Packet too large',
150: 'Message rate too high',
151: 'Quota exceeded',
152: 'Administrative action',
153: 'Payload format invalid',
154: 'Retain not supported',
155: 'QoS not supported',
156: 'Use another server',
157: 'Server moved',
158: 'Shared Subscriptions not supported',
159: 'Connection rate exceeded',
160: 'Maximum connect time',
161: 'Subscription Identifiers not supported',
162: 'Wildcard Subscriptions not supported',
},
}

type MqttVersion = '3.1' | '3.1.1' | '5.0'

const getErrorReason = (version: MqttVersion, code: number) => {
const versionMap = MqttErrorReason[version]
return versionMap[code] ?? 'Unknown error'
}

export default getErrorReason
78 changes: 32 additions & 46 deletions web/src/components/SubscriptionsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,7 @@ import Contextmenu from '@/components/Contextmenu.vue'
import { updateConnection } from '@/utils/api/connection'
import time from '@/utils/time'
import { getSubscriptionId } from '@/utils/idGenerator'

enum SubscribeErrorReason {
normal,
qosSubFailed, // qos is abnormal
qosSubSysFailed, // qos is abnormal becauseof $SYS subscribe
emptySubFailed, // subscription returns empty array
}
import getErrorReason from '@/utils/mqttErrorReason'

@Component({
components: {
Expand Down Expand Up @@ -337,28 +331,6 @@ export default class SubscriptionsList extends Vue {
})
}

/**
* Get the error reason message corresponding to the enumeration.
* Check that errorReason not equal `SubscribeErrorReason.normal` before using.
* @return Return the message of failure subscribe
* @param errorReason - Type:enum, The reason cause the failed subscription
*/
private getErrorReasonMsg(errorReason: SubscribeErrorReason): VueI18n.TranslateResult {
if (errorReason === SubscribeErrorReason.normal) return ''
switch (errorReason) {
case errorReason & SubscribeErrorReason.qosSubFailed: {
return this.$t('connections.qosSubFailed')
}
case errorReason & SubscribeErrorReason.qosSubSysFailed: {
return this.$t('connections.qosSubSysFailed')
}
case errorReason & SubscribeErrorReason.emptySubFailed: {
return this.$t('connections.emptySubFailed')
}
}
return this.$t('connections.unknowSubFailed')
}

public async resubscribe() {
this.getCurrentConnection(this.connectionId)
for (let sub of this.subsList) {
Expand Down Expand Up @@ -392,6 +364,17 @@ export default class SubscriptionsList extends Vue {
}
}

private handleSubError(topic: string, qos: number) {
const isAclSubFailed = (qos: number) => {
return [128, 135].includes(qos)
}

const aclSubFailed = isAclSubFailed(qos)
const errorReasonMsg = aclSubFailed ? `. ${this.$t('connections.aclSubFailed')}` : ''
const errorReason = getErrorReason(this.record.mqttVersion, qos)
this.$message.error(`${this.$t('connections.subFailed', [topic, errorReason, qos]) + errorReasonMsg}`)
}

public async subscribe(
{ topic, alias, qos, nl, rap, rh, subscriptionIdentifier, disabled }: SubscriptionModel,
isAuto?: boolean,
Expand All @@ -407,9 +390,10 @@ export default class SubscriptionsList extends Vue {
this.subRecord.disabled = disabled
this.subRecord.color = getRandomColor()
}
let isFinshed = false
let isFinished = false

if (this.client.subscribe) {
const topicsArr = this.multiTopics ? topic.split(',') : topic
const topicsArr = this.multiTopics ? [...new Set(topic.split(','))].filter(Boolean) : topic
const aliasArr = this.multiTopics ? alias?.split(',') : alias
let properties: { subscriptionIdentifier: number } | undefined = undefined
if (this.record.mqttVersion === '5.0' && subscriptionIdentifier) {
Expand All @@ -423,17 +407,17 @@ export default class SubscriptionsList extends Vue {
this.$message.error(error)
return false
}
let errorReason = SubscribeErrorReason.normal
if (!granted || (Array.isArray(granted) && granted.length < 1)) {
} else if (![0, 1, 2].includes(granted[0].qos) && topic.match(/^(\$SYS)/i)) {
errorReason = SubscribeErrorReason.qosSubSysFailed
} else if (![0, 1, 2].includes(granted[0].qos)) {
errorReason = SubscribeErrorReason.qosSubFailed
}
if (errorReason !== SubscribeErrorReason.normal) {
const errorReasonMsg: VueI18n.TranslateResult = this.getErrorReasonMsg(errorReason)
const errorMsg: string = `${this.$t('connections.subFailed')} ${errorReasonMsg}`
this.$message.error(errorMsg)
const successSubscriptions: string[] = []
granted.forEach((grant) => {
if ([0, 1, 2].includes(grant.qos)) {
successSubscriptions.push(grant.topic)
} else {
setTimeout(() => {
this.handleSubError(grant.topic, grant.qos)
}, 0)
}
})
if (!successSubscriptions.length) {
return false
}
if (enable) {
Expand All @@ -443,7 +427,9 @@ export default class SubscriptionsList extends Vue {
this.saveTopicToSubList(topic, qos)
} else {
topicsArr.forEach((topic, index) => {
this.saveTopicToSubList(topic, qos, index, aliasArr as string[])
if (successSubscriptions.includes(topic)) {
this.saveTopicToSubList(topic, qos, index, aliasArr as string[])
}
})
}
}
Expand All @@ -453,18 +439,18 @@ export default class SubscriptionsList extends Vue {
this.changeSubs({ id: this.connectionId, subscriptions: this.subsList })
this.showDialog = false
}
isFinshed = true
isFinished = true
})
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

// TODO: maybe we should replace mqtt.js to mqtt-async.js
await new Promise(async (resolve) => {
// long pool query base on sleep
while (!isFinshed) {
while (!isFinished) {
await sleep(100)
}
resolve(isFinshed)
resolve(isFinished)
})
}

Expand Down
Loading
Loading