完善通知重连和更新失败重试
This commit is contained in:
parent
f1546b9efc
commit
21a0275e2f
@ -35,22 +35,23 @@ export function useHome() {
|
||||
const message = ref('')
|
||||
const error = ref('')
|
||||
let eventSource = null
|
||||
let refreshTimer = null
|
||||
let refreshGeneration = 0
|
||||
|
||||
const isLoggedIn = computed(() => Boolean(user.value && session.value))
|
||||
const canUpload = computed(() => user.value?.role === 'developer' || user.value?.role === 'admin')
|
||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||
const isScoutPackage = computed(() => uploadForm.value.file?.name.toLowerCase().endsWith('.scout'))
|
||||
const platformOptions = ['windows', 'macos', 'linux', 'android', 'any']
|
||||
const architectureOptions = computed(() => {
|
||||
const platform = filters.value.platform || uploadForm.value.platform
|
||||
const options = {
|
||||
windows: ['amd64', 'arm64', '386'],
|
||||
macos: ['amd64', 'arm64'],
|
||||
linux: ['amd64', 'arm64', '386', 'armv7'],
|
||||
android: ['arm64', 'armv7', '386'],
|
||||
}
|
||||
return options[platform] || ['amd64', 'arm64', '386', 'armv7', 'universal']
|
||||
})
|
||||
const architectureOptionsByPlatform = {
|
||||
windows: ['amd64', 'arm64', '386'],
|
||||
macos: ['amd64', 'arm64'],
|
||||
linux: ['amd64', 'arm64', '386', 'armv7'],
|
||||
android: ['arm64', 'armv7', '386'],
|
||||
}
|
||||
const universalArchitectureOptions = ['amd64', 'arm64', '386', 'armv7', 'universal']
|
||||
const filterArchitectureOptions = computed(() => architectureOptionsByPlatform[filters.value.platform] || universalArchitectureOptions)
|
||||
const uploadArchitectureOptions = computed(() => architectureOptionsByPlatform[uploadForm.value.platform] || universalArchitectureOptions)
|
||||
const channelOptions = ['stable', 'beta', 'dev']
|
||||
|
||||
function roleText(role) {
|
||||
@ -110,6 +111,41 @@ export function useHome() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPackages() {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
const generation = ++refreshGeneration
|
||||
await loadPackages()
|
||||
if (generation === refreshGeneration) {
|
||||
startNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRefresh() {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer)
|
||||
}
|
||||
refreshTimer = setTimeout(() => {
|
||||
refreshTimer = null
|
||||
void refreshPackages()
|
||||
}, 180)
|
||||
}
|
||||
|
||||
function normalizeFilterArchitecture() {
|
||||
if (filters.value.architecture && !filterArchitectureOptions.value.includes(filters.value.architecture)) {
|
||||
filters.value.architecture = ''
|
||||
}
|
||||
scheduleRefresh()
|
||||
}
|
||||
|
||||
function normalizeUploadArchitecture() {
|
||||
if (uploadForm.value.architecture && !uploadArchitectureOptions.value.includes(uploadForm.value.architecture)) {
|
||||
uploadForm.value.architecture = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function selectPackage(item) {
|
||||
selectedPackage.value = item
|
||||
error.value = ''
|
||||
@ -168,7 +204,7 @@ export function useHome() {
|
||||
uploadForm.value.file = null
|
||||
const fileInput = document.querySelector('#package-file')
|
||||
if (fileInput) fileInput.value = ''
|
||||
await loadPackages()
|
||||
await refreshPackages()
|
||||
} catch (reason) {
|
||||
error.value = reason.message
|
||||
} finally {
|
||||
@ -321,6 +357,10 @@ export function useHome() {
|
||||
function stopNotifications() {
|
||||
eventSource?.close()
|
||||
eventSource = null
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@ -348,10 +388,15 @@ export function useHome() {
|
||||
isAdmin,
|
||||
isScoutPackage,
|
||||
platformOptions,
|
||||
architectureOptions,
|
||||
filterArchitectureOptions,
|
||||
uploadArchitectureOptions,
|
||||
channelOptions,
|
||||
roleText,
|
||||
loadPackages,
|
||||
refreshPackages,
|
||||
scheduleRefresh,
|
||||
normalizeFilterArchitecture,
|
||||
normalizeUploadArchitecture,
|
||||
selectPackage,
|
||||
onFileChange,
|
||||
uploadPackage,
|
||||
@ -367,4 +412,3 @@ export function useHome() {
|
||||
stopNotifications,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useHome } from './Home.js'
|
||||
import './Home.css'
|
||||
|
||||
@ -27,10 +27,15 @@ const {
|
||||
isAdmin,
|
||||
isScoutPackage,
|
||||
platformOptions,
|
||||
architectureOptions,
|
||||
filterArchitectureOptions,
|
||||
uploadArchitectureOptions,
|
||||
channelOptions,
|
||||
roleText,
|
||||
loadPackages,
|
||||
refreshPackages,
|
||||
scheduleRefresh,
|
||||
normalizeFilterArchitecture,
|
||||
normalizeUploadArchitecture,
|
||||
selectPackage,
|
||||
onFileChange,
|
||||
uploadPackage,
|
||||
@ -48,10 +53,10 @@ const {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMe()
|
||||
await loadPackages()
|
||||
startNotifications()
|
||||
await refreshPackages()
|
||||
})
|
||||
|
||||
watch(filters, scheduleRefresh, { deep: true })
|
||||
onUnmounted(stopNotifications)
|
||||
</script>
|
||||
|
||||
@ -80,16 +85,16 @@ onUnmounted(stopNotifications)
|
||||
</label>
|
||||
<label>
|
||||
<span>平台筛选</span>
|
||||
<select v-model="filters.platform">
|
||||
<select v-model="filters.platform" @change="normalizeFilterArchitecture">
|
||||
<option value="">全部平台</option>
|
||||
<option v-for="item in platformOptions" :key="item" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>架构筛选</span>
|
||||
<select v-model="filters.architecture">
|
||||
<select v-model="filters.architecture" @change="normalizeFilterArchitecture">
|
||||
<option value="">全部架构</option>
|
||||
<option v-for="item in architectureOptions" :key="item" :value="item">{{ item }}</option>
|
||||
<option v-for="item in filterArchitectureOptions" :key="item" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@ -103,7 +108,7 @@ onUnmounted(stopNotifications)
|
||||
<span>当前版本</span>
|
||||
<input v-model="filters.currentVersion" type="text" placeholder="例如:1.0.0" @keyup.enter="loadPackages" />
|
||||
</label>
|
||||
<button class="secondary-button" type="button" @click="loadPackages">刷新包列表</button>
|
||||
<button class="secondary-button" type="button" @click="refreshPackages">刷新包列表</button>
|
||||
<div v-if="isLoggedIn" class="user-summary">
|
||||
<span>{{ user.name }}({{ roleText(user.role) }})</span>
|
||||
<button class="text-button" type="button" @click="logout">退出</button>
|
||||
@ -252,16 +257,16 @@ onUnmounted(stopNotifications)
|
||||
</label>
|
||||
<label>
|
||||
<span>平台</span>
|
||||
<select v-model="uploadForm.platform" :disabled="isScoutPackage">
|
||||
<select v-model="uploadForm.platform" :disabled="isScoutPackage" @change="normalizeUploadArchitecture">
|
||||
<option value="">请选择平台</option>
|
||||
<option v-for="item in platformOptions" :key="item" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>架构</span>
|
||||
<select v-model="uploadForm.architecture" :disabled="isScoutPackage">
|
||||
<select v-model="uploadForm.architecture" :disabled="isScoutPackage" @change="normalizeUploadArchitecture">
|
||||
<option value="">请选择架构</option>
|
||||
<option v-for="item in architectureOptions" :key="item" :value="item">{{ item }}</option>
|
||||
<option v-for="item in uploadArchitectureOptions" :key="item" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
|
||||
@ -177,6 +177,68 @@ func TestHealthyCleanupUsesConfiguredRetention(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareCleansFailedStagingAndCanRetry(t *testing.T) {
|
||||
api := newTestClient(t)
|
||||
invalidManifest := scoutpkg.Manifest{
|
||||
SchemaVersion: 1,
|
||||
Name: "demo-core",
|
||||
Version: "1.1.0",
|
||||
Channel: "stable",
|
||||
PackageType: "full",
|
||||
Targets: []scoutpkg.Target{{
|
||||
Platform: "linux",
|
||||
Architecture: "amd64",
|
||||
Path: "targets/linux/amd64",
|
||||
Entrypoint: "targets/linux/amd64/missing",
|
||||
}},
|
||||
}
|
||||
if _, err := api.Upload(context.Background(), client.UploadRequest{
|
||||
FileName: "invalid.scout",
|
||||
File: bytes.NewReader(createScoutArchive(t, invalidManifest, map[string][]byte{
|
||||
"targets/linux/amd64/payload": []byte("payload"),
|
||||
})),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
manager, err := New(Config{
|
||||
Client: api,
|
||||
PackageName: "demo-core",
|
||||
RootDir: root,
|
||||
Platform: "linux",
|
||||
Architecture: "amd64",
|
||||
HealthTimeout: 10 * time.Millisecond,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := manager.Prepare(context.Background(), ""); err == nil {
|
||||
t.Fatal("入口不存在的更新包应该准备失败")
|
||||
}
|
||||
staging, err := filepath.Glob(filepath.Join(root, "releases", ".staging-*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(staging) != 0 {
|
||||
t.Fatalf("失败后不应保留暂存目录: %v", staging)
|
||||
}
|
||||
|
||||
if _, err := api.Upload(context.Background(), client.UploadRequest{
|
||||
FileName: "valid.scout",
|
||||
File: bytes.NewReader(createScoutPackage(t, "1.2.0")),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prepared, err := manager.Prepare(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("清理失败暂存目录后应允许重试: %v", err)
|
||||
}
|
||||
if prepared.Version != "1.2.0" {
|
||||
t.Fatalf("重试准备的版本错误: %s", prepared.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T) *client.Client {
|
||||
t.Helper()
|
||||
store, err := registry.New(t.TempDir())
|
||||
@ -194,8 +256,6 @@ func newTestClient(t *testing.T) *client.Client {
|
||||
|
||||
func createScoutPackage(t *testing.T, version string) []byte {
|
||||
t.Helper()
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
writer := zip.NewWriter(buffer)
|
||||
manifest := scoutpkg.Manifest{
|
||||
SchemaVersion: 1,
|
||||
Name: "demo-core",
|
||||
@ -209,6 +269,15 @@ func createScoutPackage(t *testing.T, version string) []byte {
|
||||
Entrypoint: "targets/linux/amd64/demo",
|
||||
}},
|
||||
}
|
||||
return createScoutArchive(t, manifest, map[string][]byte{
|
||||
"targets/linux/amd64/demo": []byte("new binary"),
|
||||
})
|
||||
}
|
||||
|
||||
func createScoutArchive(t *testing.T, manifest scoutpkg.Manifest, files map[string][]byte) []byte {
|
||||
t.Helper()
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
writer := zip.NewWriter(buffer)
|
||||
manifestData, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -220,12 +289,14 @@ func createScoutPackage(t *testing.T, version string) []byte {
|
||||
if _, err := entry.Write(manifestData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entry, err = writer.Create("targets/linux/amd64/demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := entry.Write([]byte("new binary")); err != nil {
|
||||
t.Fatal(err)
|
||||
for name, content := range files {
|
||||
entry, err := writer.Create(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := entry.Write(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user