Skip to content

Commit

Permalink
feat: support queue multiple app to install
Browse files Browse the repository at this point in the history
  • Loading branch information
bitxeno committed Jun 25, 2024
1 parent 26ff8ee commit 4f8a2be
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 105 deletions.
149 changes: 61 additions & 88 deletions internal/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"time"

"github.com/bitxeno/atvloadly/internal/app"
Expand All @@ -24,19 +25,22 @@ import (
var instance = new()

type Task struct {
c *cron.Cron
Running bool `json:"running"`
InstallingApp *model.InstalledApp
c *cron.Cron
InstallingApps sync.Map
InstallAppQueue chan model.InstalledApp
chExitQueue chan bool
}

func new() *Task {
return &Task{}
return &Task{
InstallAppQueue: make(chan model.InstalledApp, 100),
chExitQueue: make(chan bool, 1),
}
}

func (t *Task) RunSchedule() error {
if t.c != nil {
t.c.Stop()
t.c = nil
t.Stop()
}

if !app.Settings.Task.Enabled {
Expand All @@ -52,61 +56,52 @@ func (t *Task) RunSchedule() error {
}

log.Infof("App refresh scheduled task has started, time: %s", app.Settings.Task.CrodTime)
t.c.Start()
t.Start()

return nil
}

func (t *Task) Start() {
t.c.Start()
go t.runQueue()
}

func (t *Task) Stop() {
t.c.Stop()
t.chExitQueue <- true
<-t.c.Stop().Done()
t.c = nil
}

func (t *Task) Run() {
if t.Running {
return
}
t.startedState()
defer t.completedState()

installedApps, err := service.GetEnableAppList()
if err != nil {
log.Err(err).Msg("Failed to get the installation list")
return
}

now := time.Now()
failedList := []model.InstalledApp{}
failedMsg := ""
for _, v := range installedApps {
if !t.checkNeedRefresh(v) {
continue
}

log.Infof("Start installing ipa: %s", v.IpaName)
err := t.runInternalRetry(v)
if err != nil {
now := time.Now()
v.RefreshedDate = &now
v.RefreshedResult = false
_ = service.UpdateAppRefreshResult(v)

failedList = append(failedList, v)
failedMsg += i18n.LocalizeF("notify.batch_content", map[string]interface{}{"name": v.IpaName, "error": err.Error()})
} else {
v.RefreshedDate = &now
v.RefreshedResult = true
_ = service.UpdateAppRefreshResult(v)
}
log.Infof("Ipa installation completed. %s", v.IpaName)

// Next execution delayed by 10 seconds.
time.Sleep(10 * time.Second)
t.StartInstallApp(v)
}
}

// Send installation failure notification.
if len(failedList) > 0 {
title := i18n.LocalizeF("notify.title", map[string]interface{}{"name": "atvloadly"})
_ = notify.Send(title, failedMsg)
func (t *Task) runQueue() {
for {
select {
case v := <-t.InstallAppQueue:
log.Infof("Start installing ipa: %s", v.IpaName)
t.tryInstallApp(v)
t.InstallingApps.Delete(v.ID)

// Next execution delayed by 5 seconds.
time.Sleep(5 * time.Second)
case <-t.chExitQueue:
log.Info("Install app queue exit.")
return
}
}
}

Expand All @@ -131,15 +126,25 @@ func (t *Task) checkNeedRefresh(v model.InstalledApp) bool {
return false
}

func (t *Task) RunImmediately(v model.InstalledApp) {
if t.Running {
return
func (t *Task) StartInstallApp(v model.InstalledApp) {
t.InstallAppQueue <- v
}

func (t *Task) tryInstallApp(v model.InstalledApp) {
var err error
err = t.runInternal(v)
// AppleTV system has reboot/lockdownd sleep, try restart usbmuxd to fix
// LOCKDOWN_E_MUX_ERROR / AFC_E_MUX_ERROR /
if err != nil {
log.Infof("Try restarting usbmuxd to fix LOCKDOWN_E_MUX_ERROR error. %s", v.IpaName)
if err = manager.RestartUsbmuxd(); err == nil {
log.Infof("usbmuxd restart complete, try installing ipa again. %s", v.IpaName)
time.Sleep(5 * time.Second)
err = t.runInternal(v)
}
}
t.startedState()
defer t.completedState()

now := time.Now()
err := t.runInternalRetry(v)
if err == nil {
v.RefreshedDate = &now
v.RefreshedResult = true
Expand All @@ -156,24 +161,7 @@ func (t *Task) RunImmediately(v model.InstalledApp) {
}
}

func (t *Task) runInternalRetry(v model.InstalledApp) error {
err := t.runInternal(v)
// AppleTV system has reboot/lockdownd sleep, try restart usbmuxd to fix
// LOCKDOWN_E_MUX_ERROR / AFC_E_MUX_ERROR /
if err != nil {
log.Infof("Try restarting usbmuxd to fix LOCKDOWN_E_MUX_ERROR error. %s", v.IpaName)
if err = manager.RestartUsbmuxd(); err == nil {
log.Infof("usbmuxd restart complete, try installing ipa again. %s", v.IpaName)
time.Sleep(5 * time.Second)
err = t.runInternal(v)
}
}
return err
}

func (t *Task) runInternal(v model.InstalledApp) error {
t.InstallingApp = &v

if v.Account == "" || v.Password == "" || v.UDID == "" {
log.Info("account or password or UDID is empty")
return fmt.Errorf("account or password or UDID is empty")
Expand Down Expand Up @@ -277,41 +265,26 @@ func (t *Task) writeLog(v model.InstalledApp, data []byte) {
}
}

func (t *Task) startedState() {
t.Running = true
log.Info("Start executing installation task...")
}

func (t *Task) completedState() {
t.Running = false
t.InstallingApp = nil
log.Info("Installation task completed.")
}

func ScheduleRefreshApps() error {
return instance.RunSchedule()
}

func RunInstallApp(v model.InstalledApp) {
go instance.RunImmediately(v)
if _, loaded := instance.InstallingApps.LoadOrStore(v.ID, v); !loaded {
instance.StartInstallApp(v)
}
}

func GetCurrentInstallingApp() *model.InstalledApp {
if instance.InstallingApp == nil {
return nil
}
if !instance.Running {
return nil
}
func GetCurrentInstallingApps() []model.InstalledApp {
installingApps := []model.InstalledApp{}

return instance.InstallingApp
instance.InstallingApps.Range(func(key, value interface{}) bool {
installingApps = append(installingApps, value.(model.InstalledApp))
return true
})
return installingApps
}

func ReloadTask() error {
if instance.c != nil {
<-instance.c.Stop().Done()
instance.c = nil
}

return instance.RunSchedule()
}
3 changes: 1 addition & 2 deletions web/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,7 @@ func route(fi *fiber.App) {
})

api.Get("/apps/installing", func(c *fiber.Ctx) error {
app := task.GetCurrentInstallingApp()
return c.Status(http.StatusOK).JSON(apiSuccess(app))
return c.Status(http.StatusOK).JSON(apiSuccess(task.GetCurrentInstallingApps()))
})

api.Post("/apps", func(c *fiber.Ctx) error {
Expand Down
2 changes: 1 addition & 1 deletion web/static/src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default {
});
},

getInstallingApp: (params) => {
getInstallingApps: (params) => {
return request({
url: "/api/apps/installing",
method: "get",
Expand Down
34 changes: 20 additions & 14 deletions web/static/src/page/home/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export default {
// },
],
services: [],
installingApp: null,
installingApps: [],
checkInstallingTimer: null,
};
},
Expand Down Expand Up @@ -296,14 +296,14 @@ export default {
checkInstallingApp() {
let _this = this;
api.getInstallingApp().then((res) => {
api.getInstallingApps().then((res) => {
// res.data returns empty, indicating that the installation has been completed.
if (_this.installingApp && !res.data) {
if (_this.installingApps && !res.data) {
_this.fetchAppList();
}
_this.installingApp = res.data;
if (_this.installingApp) {
_this.installingApps = res.data;
if (_this.installingApps && _this.installingApps.length > 0) {
// Repeat the detection until it is completed.
_this.checkInstallingAppDelay();
}
Expand Down Expand Up @@ -332,7 +332,7 @@ export default {
refreshApp(item) {
let _this = this;
_this.installingApp = item;
_this.installingApps.push(item);
api.refreshApp(item.ID).then((res) => {
_this.checkInstallingAppDelay();
Expand Down Expand Up @@ -371,32 +371,33 @@ export default {
}
},
formatRefreshDate(item) {
if (this.installingApp && this.installingApp.ID == item.ID) {
return this.$t("home.table.refresh_date_format.installing_tips");
let _this = this;
if (_this.isInstalling(item)) {
return _this.$t("home.table.refresh_date_format.installing_tips");
}
if (!item.refreshed_date) return "-";
let seconds = dayjs().diff(dayjs(item.refreshed_date), "second");
if (seconds < 60) {
return this.$t("home.table.refresh_date_format.seconds", {
return _this.$t("home.table.refresh_date_format.seconds", {
num: seconds,
});
}
let miniutes = parseInt(seconds / 60, 10);
if (miniutes < 60) {
return this.$t("home.table.refresh_date_format.miniutes", {
return _this.$t("home.table.refresh_date_format.miniutes", {
num: miniutes,
});
}
let hours = parseInt(seconds / 3600, 10);
if (hours < 24) {
return this.$t("home.table.refresh_date_format.hours", {
return _this.$t("home.table.refresh_date_format.hours", {
num: hours,
});
}
let days = parseInt(seconds / 24 / 3600, 10);
return this.$t("home.table.refresh_date_format.days", {
return _this.$t("home.table.refresh_date_format.days", {
num: days,
});
},
Expand All @@ -418,9 +419,14 @@ export default {
return this.$t("home.sidebar.device_status.unpaired");
},
isInstalling(item) {
if (!this.installingApp) return false;
if (!this.installingApps || this.installingApps.length == 0) return false;
return item.ID == this.installingApp.ID;
for (let i = 0; i < this.installingApps.length; i++) {
if( this.installingApps[i].ID == item.ID) {
return true;
}
}
return false;
},
iconUrl(app) {
if (app.icon) {
Expand Down

0 comments on commit 4f8a2be

Please sign in to comment.