@@ -116,6 +116,9 @@ export default {
}
},
computed: {
+ ...mapState('setting', [
+ 'setting'
+ ]),
...mapState('lang', [
'lang'
]),
@@ -136,7 +139,7 @@ export default {
this.$router.push({ path: this.redirect || '/' })
this.$store.dispatch('user/getInfo')
}).catch(() => {
- this.$message.error(this.$t('Error when logging in (Please check username and password)'))
+ this.$message.error(this.$t('Error when logging in (Please read documentation Q&A)'))
this.loading = false
})
}
diff --git a/frontend/src/views/node/NodeList.vue b/frontend/src/views/node/NodeList.vue
index 9ea51502..e032bcd4 100644
--- a/frontend/src/views/node/NodeList.vue
+++ b/frontend/src/views/node/NodeList.vue
@@ -202,7 +202,7 @@ export default {
},
onRefresh () {
this.$store.dispatch('node/getNodeList')
- this.$st.sendEv('节点', '刷新')
+ this.$st.sendEv('节点列表', '刷新')
},
onSubmit () {
const vm = this
@@ -246,13 +246,13 @@ export default {
message: 'Deleted successfully'
})
})
- this.$st.sendEv('节点', '删除', 'id', row._id)
+ this.$st.sendEv('节点列表', '删除节点')
})
},
onView (row) {
this.$router.push(`/nodes/${row._id}`)
- this.$st.sendEv('节点', '查看', 'id', row._id)
+ this.$st.sendEv('节点列表', '查看节点')
},
onPageChange () {
this.$store.dispatch('node/getNodeList')
diff --git a/frontend/src/views/schedule/ScheduleList.vue b/frontend/src/views/schedule/ScheduleList.vue
index 0259247e..23d9fa97 100644
--- a/frontend/src/views/schedule/ScheduleList.vue
+++ b/frontend/src/views/schedule/ScheduleList.vue
@@ -14,9 +14,15 @@
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -79,7 +74,7 @@
-
+
@@ -120,6 +115,13 @@
+
+
+ {{$t('All Nodes')}}
+ {{$t('Selected Nodes')}}
+ {{$t('Random')}}
+
+
-
+
@@ -165,7 +167,8 @@ export default {
return {
columns: [
{ name: 'name', label: 'Name', width: '180' },
- { name: 'cron', label: 'schedules.cron', width: '120' },
+ { name: 'cron', label: 'Cron', width: '120' },
+ { name: 'run_type', label: 'Run Type', width: '150' },
{ name: 'node_name', label: 'Node', width: '150' },
{ name: 'spider_name', label: 'Spider', width: '150' },
{ name: 'param', label: 'Parameters', width: '150' },
@@ -208,8 +211,8 @@ export default {
onAdd () {
this.isEdit = false
this.dialogVisible = true
- this.$store.commit('schedule/SET_SCHEDULE_FORM', {})
- this.$st.sendEv('定时任务', '添加')
+ this.$store.commit('schedule/SET_SCHEDULE_FORM', { node_ids: [] })
+ this.$st.sendEv('定时任务', '添加定时任务')
},
onAddSubmit () {
this.$refs.scheduleForm.validate(res => {
@@ -235,7 +238,7 @@ export default {
}
}
})
- this.$st.sendEv('定时任务', '提交')
+ this.$st.sendEv('定时任务', '提交定时任务')
},
isShowRun (row) {
},
@@ -243,12 +246,12 @@ export default {
this.$store.commit('schedule/SET_SCHEDULE_FORM', row)
this.dialogVisible = true
this.isEdit = true
- this.$st.sendEv('定时任务', '修改', 'id', row._id)
+ this.$st.sendEv('定时任务', '修改定时任务')
},
onRemove (row) {
- this.$confirm('确定删除定时任务?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
+ this.$confirm(this.$t('Are you sure to delete the schedule task?'), this.$t('Notification'), {
+ confirmButtonText: this.$t('Confirm'),
+ cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('schedule/removeSchedule', row._id)
@@ -258,15 +261,16 @@ export default {
this.$message.success(`Schedule "${row.name}" has been removed`)
}, 100)
})
- }).catch(() => {})
- this.$st.sendEv('定时任务', '删除', 'id', row._id)
+ }).catch(() => {
+ })
+ this.$st.sendEv('定时任务', '删除定时任务')
},
onCrawl (row) {
// 停止定时任务
if (!row.status || row.status === 'running') {
- this.$confirm('确定停止定时任务?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
+ this.$confirm(this.$t('Are you sure to delete the schedule task?'), this.$t('Notification'), {
+ confirmButtonText: this.$t('Confirm'),
+ cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('schedule/stopSchedule', row._id)
@@ -280,13 +284,14 @@ export default {
message: resp.data.error
})
})
- }).catch(() => {})
+ }).catch(() => {
+ })
}
// 运行定时任务
if (row.status === 'stop') {
- this.$confirm('确定运行定时任务?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
+ this.$confirm(this.$t('Are you sure to delete the schedule task?'), this.$t('Notification'), {
+ confirmButtonText: this.$t('Confirm'),
+ cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('schedule/runSchedule', row._id)
@@ -300,7 +305,24 @@ export default {
message: resp.data.error
})
})
- }).catch(() => {})
+ }).catch(() => {
+ })
+ }
+ },
+ isDisabledSpider (spider) {
+ if (spider.type === 'customized') {
+ return !spider.cmd
+ } else {
+ return false
+ }
+ },
+ getStatusTooltip (row) {
+ if (row.status === 'stop') {
+ return 'Start'
+ } else if (row.status === 'running') {
+ return 'Stop'
+ } else if (row.status === 'error') {
+ return 'Start'
}
}
},
@@ -338,6 +360,7 @@ export default {
min-height: 360px;
margin-top: 10px;
}
+
.status-tag {
cursor: pointer;
}
diff --git a/frontend/src/views/spider/SpiderList.vue b/frontend/src/views/spider/SpiderList.vue
index 78c87a36..74177b62 100644
--- a/frontend/src/views/spider/SpiderList.vue
+++ b/frontend/src/views/spider/SpiderList.vue
@@ -370,17 +370,17 @@ export default {
}
await this.$store.dispatch('spider/getSpiderList')
this.$router.push(`/spiders/${res2.data.data._id}`)
- this.$st.sendEv('爬虫', '添加爬虫-可配置爬虫')
+ this.$st.sendEv('爬虫列表', '添加爬虫', '可配置爬虫')
})
},
onAddCustomized () {
this.addDialogVisible = false
this.addCustomizedDialogVisible = true
- this.$st.sendEv('爬虫', '添加爬虫-自定义爬虫')
+ this.$st.sendEv('爬虫列表', '添加爬虫', '自定义爬虫')
},
onRefresh () {
this.getList()
- this.$st.sendEv('爬虫', '刷新')
+ this.$st.sendEv('爬虫列表', '刷新')
},
onSubmit () {
const vm = this
@@ -434,19 +434,19 @@ export default {
message: 'Deleted successfully'
})
})
- this.$st.sendEv('爬虫', '删除')
+ this.$st.sendEv('爬虫列表', '删除爬虫')
})
},
onCrawl (row, ev) {
ev.stopPropagation()
this.crawlConfirmDialogVisible = true
this.activeSpiderId = row._id
- this.$st.sendEv('爬虫', '点击运行')
+ this.$st.sendEv('爬虫列表', '点击运行')
},
onView (row, ev) {
ev.stopPropagation()
this.$router.push('/spiders/' + row._id)
- this.$st.sendEv('爬虫', '查看')
+ this.$st.sendEv('爬虫列表', '查看爬虫')
},
onImport () {
this.$refs.importForm.validate(valid => {
@@ -467,7 +467,7 @@ export default {
})
}
})
- this.$st.sendEv('爬虫', '导入爬虫')
+ this.$st.sendEv('爬虫列表', '导入爬虫')
},
openImportDialog () {
this.dialogVisible = true
@@ -495,10 +495,6 @@ export default {
callback(data)
})
},
- onSiteSelect (item) {
- this.$store.commit('spider/SET_FILTER_SITE', item._id)
- this.$st.sendEv('爬虫', '搜索网站')
- },
onAddConfigurableSiteSelect (item) {
this.spiderForm.site = item._id
},
diff --git a/frontend/src/views/task/TaskDetail.vue b/frontend/src/views/task/TaskDetail.vue
index d61394e8..9097344e 100644
--- a/frontend/src/views/task/TaskDetail.vue
+++ b/frontend/src/views/task/TaskDetail.vue
@@ -97,7 +97,7 @@ export default {
},
downloadCSV () {
this.$store.dispatch('task/getTaskResultExcel', this.$route.params.id)
- this.$st.sendEv('任务详情-结果', '下载CSV')
+ this.$st.sendEv('任务详情', '结果', '下载CSV')
},
getTaskLog () {
if (this.$route.params.id) {
diff --git a/frontend/src/views/task/TaskList.vue b/frontend/src/views/task/TaskList.vue
index 8013c080..becc7d0b 100644
--- a/frontend/src/views/task/TaskList.vue
+++ b/frontend/src/views/task/TaskList.vue
@@ -221,7 +221,7 @@ export default {
},
onRefresh () {
this.$store.dispatch('task/getTaskList')
- this.$st.sendEv('任务', '搜索')
+ this.$st.sendEv('任务列表', '搜索')
},
onRemoveMultipleTask () {
if (this.multipleSelection.length === 0) {
@@ -267,20 +267,20 @@ export default {
message: 'Deleted successfully'
})
})
- this.$st.sendEv('任务', '删除', 'id', row._id)
+ this.$st.sendEv('任务列表', '删除任务')
})
},
onView (row) {
this.$router.push(`/tasks/${row._id}`)
- this.$st.sendEv('任务', '搜索', 'id', row._id)
+ this.$st.sendEv('任务列表', '查看任务')
},
onClickSpider (row) {
this.$router.push(`/spiders/${row.spider_id}`)
- this.$st.sendEv('任务', '点击爬虫详情', 'id', row.spider_id)
+ this.$st.sendEv('任务列表', '点击爬虫详情')
},
onClickNode (row) {
this.$router.push(`/nodes/${row.node_id}`)
- this.$st.sendEv('任务', '点击节点详情', 'id', row.node_id)
+ this.$st.sendEv('任务列表', '点击节点详情')
},
onPageChange () {
setTimeout(() => {
diff --git a/frontend/src/views/user/UserList.vue b/frontend/src/views/user/UserList.vue
index a0e2029f..26cbedea 100644
--- a/frontend/src/views/user/UserList.vue
+++ b/frontend/src/views/user/UserList.vue
@@ -3,14 +3,14 @@
-
-
+
+
-
+
-
-
+
+
@@ -27,7 +27,7 @@
@@ -109,6 +109,7 @@ export default {
}
return {
dialogVisible: false,
+ isAdd: false,
rules: {
password: [{ validator: validatePass }]
}
@@ -145,6 +146,7 @@ export default {
return dayjs(ts).format('YYYY-MM-DD HH:mm:ss')
},
onEdit (row) {
+ this.isAdd = false
this.$store.commit('user/SET_USER_FORM', row)
this.dialogVisible = true
},
@@ -161,24 +163,48 @@ export default {
message: this.$t('Deleted successfully')
})
})
- this.$st.sendEv('用户', '删除', 'id', row._id)
+ .then(() => {
+ this.$store.dispatch('user/getUserList')
+ })
+ this.$st.sendEv('用户列表', '删除用户')
})
// this.$store.commit('user/SET_USER_FORM', row)
},
onConfirm () {
- this.dialogVisible = false
this.$refs.form.validate(valid => {
- if (valid) {
+ if (!valid) return
+ if (this.isAdd) {
+ // 添加用户
+ this.$store.dispatch('user/addUser')
+ .then(() => {
+ this.$message({
+ type: 'success',
+ message: this.$t('Saved successfully')
+ })
+ this.dialogVisible = false
+ this.$st.sendEv('用户列表', '添加用户')
+ })
+ .then(() => {
+ this.$store.dispatch('user/getUserList')
+ })
+ } else {
+ // 编辑用户
this.$store.dispatch('user/editUser')
.then(() => {
this.$message({
type: 'success',
message: this.$t('Saved successfully')
})
+ this.dialogVisible = false
+ this.$st.sendEv('用户列表', '编辑用户')
})
}
})
- this.$st.sendEv('用户', '编辑')
+ },
+ onClickAddUser () {
+ this.isAdd = true
+ this.$store.commit('user/SET_USER_FORM', {})
+ this.dialogVisible = true
}
},
created () {
@@ -192,15 +218,18 @@ export default {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
- .filter-search {
- width: 240px;
- }
- .right {
- .btn {
- margin-left: 10px;
- }
- }
+ .filter-search {
+ width: 240px;
+ }
+
+ .right {
+
+ .btn {
+ margin-left: 10px;
+ }
+
+ }
}
.el-table {
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index ef361b2b..a6600a96 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1158,6 +1158,11 @@ ansi-regex@^4.0.0:
version "4.0.0"
resolved "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
+ansi-regex@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-4.1.0.tgz?cache=0&sync_timestamp=1570188570027&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-regex%2Fdownload%2Fansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+ integrity sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=
+
ansi-styles@^2.2.1:
version "2.2.1"
resolved "http://registry.npm.taobao.org/ansi-styles/download/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@@ -2086,6 +2091,15 @@ cliui@^4.0.0, cliui@^4.1.0:
strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
+cliui@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npm.taobao.org/cliui/download/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+ integrity sha1-3u/P2y6AB4SqNPRvoI4GhRx7u8U=
+ dependencies:
+ string-width "^3.1.0"
+ strip-ansi "^5.2.0"
+ wrap-ansi "^5.1.0"
+
clone-deep@^2.0.1:
version "2.0.2"
resolved "http://registry.npm.taobao.org/clone-deep/download/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
@@ -3884,6 +3898,11 @@ get-caller-file@^1.0.1:
version "1.0.3"
resolved "http://registry.npm.taobao.org/get-caller-file/download/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.npm.taobao.org/get-caller-file/download/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=
+
get-stdin@^4.0.1:
version "4.0.1"
resolved "http://registry.npm.taobao.org/get-stdin/download/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -7271,6 +7290,11 @@ require-main-filename@^1.0.1:
version "1.0.1"
resolved "http://registry.npm.taobao.org/require-main-filename/download/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npm.taobao.org/require-main-filename/download/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=
+
require-uncached@^1.0.3:
version "1.0.3"
resolved "http://registry.npm.taobao.org/require-uncached/download/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
@@ -7586,6 +7610,13 @@ shellwords@^0.1.1:
version "0.1.1"
resolved "http://registry.npm.taobao.org/shellwords/download/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+showdown@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.npm.taobao.org/showdown/download/showdown-1.9.1.tgz#134e148e75cd4623e09c21b0511977d79b5ad0ef"
+ integrity sha1-E04UjnXNRiPgnCGwURl315ta0O8=
+ dependencies:
+ yargs "^14.2"
+
sigmund@^1.0.1:
version "1.0.1"
resolved "http://registry.npm.taobao.org/sigmund/download/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
@@ -7890,6 +7921,15 @@ string-width@^3.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.0.0"
+string-width@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npm.taobao.org/string-width/download/string-width-3.1.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstring-width%2Fdownload%2Fstring-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+ integrity sha1-InZ74htirxCBV0MG9prFG2IgOWE=
+ dependencies:
+ emoji-regex "^7.0.1"
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^5.1.0"
+
string.prototype.padend@^3.0.0:
version "3.0.0"
resolved "http://registry.npm.taobao.org/string.prototype.padend/download/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
@@ -7940,6 +7980,13 @@ strip-ansi@^5.0.0:
dependencies:
ansi-regex "^4.0.0"
+strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+ integrity sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=
+ dependencies:
+ ansi-regex "^4.1.0"
+
strip-bom@3.0.0, strip-bom@^3.0.0:
version "3.0.0"
resolved "http://registry.npm.taobao.org/strip-bom/download/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -8834,6 +8881,15 @@ wrap-ansi@^2.0.0:
string-width "^1.0.1"
strip-ansi "^3.0.1"
+wrap-ansi@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+ integrity sha1-H9H2cjXVttD+54EFYAG/tpTAOwk=
+ dependencies:
+ ansi-styles "^3.2.0"
+ string-width "^3.0.0"
+ strip-ansi "^5.0.0"
+
wrappy@1:
version "1.0.2"
resolved "http://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -8911,6 +8967,14 @@ yargs-parser@^11.1.1:
camelcase "^5.0.0"
decamelize "^1.2.0"
+yargs-parser@^15.0.0:
+ version "15.0.0"
+ resolved "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08"
+ integrity sha1-zdepdJDsg2GV9Z8/Tb5eqej3Xwg=
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
yargs-parser@^5.0.0:
version "5.0.0"
resolved "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
@@ -8974,6 +9038,23 @@ yargs@^12.0.5:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
+yargs@^14.2:
+ version "14.2.2"
+ resolved "https://registry.npm.taobao.org/yargs/download/yargs-14.2.2.tgz?cache=0&sync_timestamp=1574137859196&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs%2Fdownload%2Fyargs-14.2.2.tgz#2769564379009ff8597cdd38fba09da9b493c4b5"
+ integrity sha1-J2lWQ3kAn/hZfN04+6CdqbSTxLU=
+ dependencies:
+ cliui "^5.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^3.0.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^15.0.0"
+
yargs@^7.0.0:
version "7.1.0"
resolved "http://registry.npm.taobao.org/yargs/download/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
diff --git a/jenkins/develop/docker-compose.yaml b/jenkins/develop/docker-compose.yaml
index ec95ae9f..745c0bdc 100644
--- a/jenkins/develop/docker-compose.yaml
+++ b/jenkins/develop/docker-compose.yaml
@@ -27,10 +27,6 @@ services:
mongo:
image: mongo:latest
restart: always
- ports:
- - "27027:27017"
redis:
image: redis:latest
restart: always
- ports:
- - "6389:6379"
\ No newline at end of file
diff --git a/jenkins/master/docker-compose.yaml b/jenkins/master/docker-compose.yaml
index 1b7a476b..ff9dd64e 100644
--- a/jenkins/master/docker-compose.yaml
+++ b/jenkins/master/docker-compose.yaml
@@ -29,12 +29,9 @@ services:
restart: always
volumes:
- "/opt/crawlab/mongo/data/db:/data/db"
- ports:
- - "27017:27017"
+ - "/opt/crawlab/mongo/tmp:/tmp"
redis:
image: redis:latest
restart: always
volumes:
- "/opt/crawlab/redis/data:/data"
- ports:
- - "6379:6379"
\ No newline at end of file
diff --git a/wait-for-it.sh b/wait-for-it.sh
new file mode 100755
index 00000000..607a7d67
--- /dev/null
+++ b/wait-for-it.sh
@@ -0,0 +1,178 @@
+#!/usr/bin/env bash
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ WAITFORIT_BUSYTIMEFLAG="-t"
+
+else
+ WAITFORIT_ISBUSY=0
+ WAITFORIT_BUSYTIMEFLAG=""
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
\ No newline at end of file