wip(0.S3): the packages migrated by ludic migrate state packages - every package test green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e8b5b0523
commit
07505e7ef2
294 changed files with 14996 additions and 14675 deletions
|
|
@ -1,49 +1,49 @@
|
|||
# chain.ludic - the plan made: the files to fetch, in order, and the base the first delta patches
|
||||
function update__plan_clear() -> void {
|
||||
update__files = new []string
|
||||
update__sha = new []string
|
||||
update__size = new []int
|
||||
update__delta = new []bool
|
||||
update__ver = new []string
|
||||
update__full = new []string
|
||||
update__total_bytes = 0
|
||||
update__done_bytes = 0
|
||||
update__i = 0
|
||||
function update__plan_clear(update_st: mut UpdateState) -> void {
|
||||
update_st.update__files = new []string
|
||||
update_st.update__sha = new []string
|
||||
update_st.update__size = new []int
|
||||
update_st.update__delta = new []bool
|
||||
update_st.update__ver = new []string
|
||||
update_st.update__full = new []string
|
||||
update_st.update__total_bytes = 0
|
||||
update_st.update__done_bytes = 0
|
||||
update_st.update__i = 0
|
||||
}
|
||||
|
||||
# the plan from a parsed feed; false when there is nothing newer than this version
|
||||
export function update_plan(feed: Val) -> bool {
|
||||
update__plan_clear()
|
||||
let as = update__assets(feed)
|
||||
export function update_plan(update_st: mut UpdateState, feed: Val) -> bool {
|
||||
update__plan_clear(update_st)
|
||||
let as = update__assets(update_st, feed)
|
||||
let n = update__newest_full(as)
|
||||
if n < 0 or not update_newer(as[n].ver, update__conf().version) { return false }
|
||||
if n < 0 or not update_newer(as[n].ver, update__conf(update_st).version) { return false }
|
||||
let newest = as[n]
|
||||
update__target = newest.ver
|
||||
let base = update_packages() + update__sep() + update__full_name(as, update__conf().version)
|
||||
let chain = update__chain(as, newest.ver)
|
||||
update_st.update__target = newest.ver
|
||||
let base = update_packages(update_st) + update__sep() + update__full_name(update_st, as, update__conf(update_st).version)
|
||||
let chain = update__chain(update_st, as, newest.ver)
|
||||
var chain_bytes = 0
|
||||
for i in 0 .. len(chain) { chain_bytes += chain[i].size }
|
||||
let whole = len(chain) > 0 and len(chain) == update__fulls_after(as)
|
||||
let whole = len(chain) > 0 and len(chain) == update__fulls_after(update_st, as)
|
||||
if Fs.exists(base) and whole and chain_bytes < newest.size {
|
||||
for i in 0 .. len(chain) { update__plan_add(chain[i], update__full_name(as, chain[i].ver)) }
|
||||
update__old = base
|
||||
for i in 0 .. len(chain) { update__plan_add(update_st, chain[i], update__full_name(update_st, as, chain[i].ver)) }
|
||||
update_st.update__old = base
|
||||
} else {
|
||||
update__plan_add(newest, newest.file)
|
||||
update__old = ""
|
||||
update__plan_add(update_st, newest, newest.file)
|
||||
update_st.update__old = ""
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
# the plan, for a panel or a test
|
||||
export function update_package_count() -> int {
|
||||
if update__files == null { return 0 }
|
||||
return len(update__files)
|
||||
export function update_package_count(update_st: UpdateState) -> int {
|
||||
if update_st.update__files == null { return 0 }
|
||||
return len(update_st.update__files)
|
||||
}
|
||||
|
||||
export function update_plan_file(i: int) -> string { return update__files[i] }
|
||||
export function update_plan_is_delta(i: int) -> bool { return update__delta[i] }
|
||||
export function update_plan_version(i: int) -> string { return update__ver[i] }
|
||||
export function update_target() -> string { return update__target }
|
||||
export function update_total_bytes() -> int { return update__total_bytes }
|
||||
export function update_plan_file(update_st: UpdateState, i: int) -> string { return update_st.update__files[i] }
|
||||
export function update_plan_is_delta(update_st: UpdateState, i: int) -> bool { return update_st.update__delta[i] }
|
||||
export function update_plan_version(update_st: UpdateState, i: int) -> string { return update_st.update__ver[i] }
|
||||
export function update_target(update_st: UpdateState) -> string { return update_st.update__target }
|
||||
export function update_total_bytes(update_st: UpdateState) -> int { return update_st.update__total_bytes }
|
||||
|
||||
function update__path(i: int) -> string { return update_packages() + update__sep() + update__files[i] }
|
||||
function update__path(update_st: mut UpdateState, i: int) -> string { return update_packages(update_st) + update__sep() + update_st.update__files[i] }
|
||||
|
|
|
|||
|
|
@ -44,19 +44,19 @@ function update__up(p: string) -> string {
|
|||
# the executable's directory
|
||||
export function update_dir() -> string { return update__up(UpdateWorld.exe()) }
|
||||
|
||||
export function update_root() -> string {
|
||||
let c = update__conf()
|
||||
export function update_root(update_st: mut UpdateState) -> string {
|
||||
let c = update__conf(update_st)
|
||||
if len(c.root) > 0 { return c.root }
|
||||
if update__mac() { return update__up(update__up(update_dir())) }
|
||||
return update_dir() + "\\.."
|
||||
}
|
||||
|
||||
# where the packages live: never inside a macOS bundle, which is read-only and which apply replaces
|
||||
export function update_packages() -> string {
|
||||
let c = update__conf()
|
||||
export function update_packages(update_st: mut UpdateState) -> string {
|
||||
let c = update__conf(update_st)
|
||||
if len(c.packages) > 0 { return c.packages }
|
||||
if update__mac() and len(c.mac_packages) > 0 { return c.mac_packages }
|
||||
return update_root() + update__sep() + "packages"
|
||||
return update_root(update_st) + update__sep() + "packages"
|
||||
}
|
||||
|
||||
function update__feed_file() -> string {
|
||||
|
|
@ -64,9 +64,9 @@ function update__feed_file() -> string {
|
|||
return "releases.win.json"
|
||||
}
|
||||
|
||||
function update__updater() -> string {
|
||||
if update__mac() { return update_root() + "/Contents/MacOS/UpdateMac" }
|
||||
return update_root() + update__sep() + "Update.exe"
|
||||
function update__updater(update_st: mut UpdateState) -> string {
|
||||
if update__mac() { return update_root(update_st) + "/Contents/MacOS/UpdateMac" }
|
||||
return update_root(update_st) + update__sep() + "Update.exe"
|
||||
}
|
||||
|
||||
# A quarantined Mac app still where it was downloaded runs from a read-only /AppTranslocation/
|
||||
|
|
@ -76,8 +76,8 @@ export function update_translocated() -> bool {
|
|||
}
|
||||
|
||||
# can this copy update itself? A copy Velopack did not install downloads the game again instead.
|
||||
export function update_installed() -> bool {
|
||||
if len(update__conf().root) > 0 { return true }
|
||||
export function update_installed(update_st: mut UpdateState) -> bool {
|
||||
if len(update__conf(update_st).root) > 0 { return true }
|
||||
if update_translocated() { return false }
|
||||
if update__mac() { return Fs.exists(update_dir() + "/UpdateMac") }
|
||||
return Fs.exists(update_dir() + "/../Update.exe")
|
||||
|
|
|
|||
|
|
@ -1,73 +1,69 @@
|
|||
# latest.ludic - "a new version is out": latest.json asked once, polled a frame at a time, so a
|
||||
# slow or absent network costs the frame nothing - it simply never says there is one.
|
||||
# latest.json is {"version": "0.5.1", "notes": {"en": ["...", ...], "tr": [...]}}
|
||||
var update__ask: int = 0
|
||||
var update__ask_h: int = -1
|
||||
var update__latest: string = ""
|
||||
var update__notes: Val = null
|
||||
|
||||
# once a frame on the screen that shows it; with show_current, the notes of the version being
|
||||
# played are kept too (a launcher shows them)
|
||||
export function update_check_tick(show_current: bool) -> void {
|
||||
if update__ask == UPDATE_ASK_IDLE {
|
||||
update__ask_h = UpdateNet.get(update__conf().feed + "/" + update__conf().latest)
|
||||
update__ask = UPDATE_ASKING
|
||||
if update__ask_h < 0 { update__ask = UPDATE_NONE }
|
||||
export function update_check_tick(update_st: mut UpdateState, show_current: bool) -> void {
|
||||
if update_st.update__ask == UPDATE_ASK_IDLE {
|
||||
update_st.update__ask_h = UpdateNet.get(update__conf(update_st).feed + "/" + update__conf(update_st).latest)
|
||||
update_st.update__ask = UPDATE_ASKING
|
||||
if update_st.update__ask_h < 0 { update_st.update__ask = UPDATE_NONE }
|
||||
return
|
||||
}
|
||||
if update__ask != UPDATE_ASKING { return }
|
||||
let st = UpdateNet.poll(update__ask_h)
|
||||
if update_st.update__ask != UPDATE_ASKING { return }
|
||||
let st = UpdateNet.poll(update_st.update__ask_h)
|
||||
if st < 0 { return }
|
||||
update__ask = UPDATE_NONE
|
||||
if st > 0 { update__latest_read(Json.parse(UpdateNet.text(update__ask_h)), show_current) }
|
||||
UpdateNet.free(update__ask_h)
|
||||
update__ask_h = -1
|
||||
update_st.update__ask = UPDATE_NONE
|
||||
if st > 0 { update__latest_read(update_st, Json.parse(UpdateNet.text(update_st.update__ask_h)), show_current) }
|
||||
UpdateNet.free(update_st.update__ask_h)
|
||||
update_st.update__ask_h = -1
|
||||
}
|
||||
|
||||
function update__latest_read(v: Val, show_current: bool) -> void {
|
||||
function update__latest_read(update_st: mut UpdateState, v: Val, show_current: bool) -> void {
|
||||
if v == null or Value.kind(v) != 6 or Value.has(v, "version") == 0 { return }
|
||||
let ver = Value.as_str(Value.get(v, "version"))
|
||||
let mine = update__conf().version
|
||||
if update_newer(ver, mine) { update__ask = UPDATE_NEWER }
|
||||
else if ver == mine and show_current { update__ask = UPDATE_CURRENT }
|
||||
let mine = update__conf(update_st).version
|
||||
if update_newer(ver, mine) { update_st.update__ask = UPDATE_NEWER }
|
||||
else if ver == mine and show_current { update_st.update__ask = UPDATE_CURRENT }
|
||||
else { return }
|
||||
update__latest = ver
|
||||
if Value.has(v, "notes") != 0 { update__notes = Value.get(v, "notes") }
|
||||
update_st.update__latest = ver
|
||||
if Value.has(v, "notes") != 0 { update_st.update__notes = Value.get(v, "notes") }
|
||||
}
|
||||
|
||||
export function update_ask_state() -> int { return update__ask }
|
||||
export function update_latest() -> string { return update__latest }
|
||||
export function update_shown() -> bool { return update__ask == UPDATE_NEWER or update__ask == UPDATE_CURRENT }
|
||||
export function update_fresh() -> bool { return update__ask == UPDATE_NEWER }
|
||||
export function update_ask_state(update_st: UpdateState) -> int { return update_st.update__ask }
|
||||
export function update_latest(update_st: UpdateState) -> string { return update_st.update__latest }
|
||||
export function update_shown(update_st: UpdateState) -> bool { return update_st.update__ask == UPDATE_NEWER or update_st.update__ask == UPDATE_CURRENT }
|
||||
export function update_fresh(update_st: UpdateState) -> bool { return update_st.update__ask == UPDATE_NEWER }
|
||||
|
||||
# the notes in this language, else English, else none
|
||||
export function update_notes(lang: string) -> []string {
|
||||
export function update_notes(update_st: UpdateState, lang: string) -> []string {
|
||||
let out = new []string
|
||||
if update__notes == null or Value.kind(update__notes) != 6 { return out }
|
||||
if update_st.update__notes == null or Value.kind(update_st.update__notes) != 6 { return out }
|
||||
var key = lang
|
||||
if Value.has(update__notes, key) == 0 { key = "en" }
|
||||
if Value.has(update__notes, key) == 0 { return out }
|
||||
let l = Value.get(update__notes, key)
|
||||
if Value.has(update_st.update__notes, key) == 0 { key = "en" }
|
||||
if Value.has(update_st.update__notes, key) == 0 { return out }
|
||||
let l = Value.get(update_st.update__notes, key)
|
||||
for i in 0 .. Value.count(l) { push(out, Value.as_str(Value.at(l, i))) }
|
||||
return out
|
||||
}
|
||||
|
||||
# back to before anything was asked or installed (tests)
|
||||
export function update_reset() -> void {
|
||||
if update__ask_h >= 0 { UpdateNet.free(update__ask_h) }
|
||||
update__ask_h = -1
|
||||
update__ask = UPDATE_ASK_IDLE
|
||||
update__latest = ""
|
||||
update__notes = null
|
||||
update__release()
|
||||
update__state = UPDATE_IDLE
|
||||
update__err = UPDATE_ERR_NONE
|
||||
update__target = ""
|
||||
update__files = null
|
||||
update__total_bytes = 0
|
||||
update__done_bytes = 0
|
||||
update__i = 0
|
||||
update__rate = 0
|
||||
update__rate_t = -1
|
||||
q_clear(update_facts())
|
||||
export function update_reset(base_st: mut BaseState, update_st: mut UpdateState) -> void {
|
||||
if update_st.update__ask_h >= 0 { UpdateNet.free(update_st.update__ask_h) }
|
||||
update_st.update__ask_h = -1
|
||||
update_st.update__ask = UPDATE_ASK_IDLE
|
||||
update_st.update__latest = ""
|
||||
update_st.update__notes = null
|
||||
update__release(update_st)
|
||||
update_st.update__state = UPDATE_IDLE
|
||||
update_st.update__err = UPDATE_ERR_NONE
|
||||
update_st.update__target = ""
|
||||
update_st.update__files = null
|
||||
update_st.update__total_bytes = 0
|
||||
update_st.update__done_bytes = 0
|
||||
update_st.update__i = 0
|
||||
update_st.update__rate = 0
|
||||
update_st.update__rate_t = -1
|
||||
q_clear(base_st, update_facts(base_st, update_st))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ property UpdateAsset {
|
|||
}
|
||||
|
||||
# this app's packages in the feed ({"Assets": [{PackageId, Version, Type, FileName, SHA256, Size}]})
|
||||
function update__assets(feed: Val) -> []UpdateAsset {
|
||||
function update__assets(update_st: mut UpdateState, feed: Val) -> []UpdateAsset {
|
||||
let out = new []UpdateAsset
|
||||
if feed == null or Value.kind(feed) != 6 or Value.has(feed, "Assets") == 0 { return out }
|
||||
let all = Value.get(feed, "Assets")
|
||||
for i in 0 .. Value.count(all) {
|
||||
let a = Value.at(all, i)
|
||||
if sv_str(a, "PackageId", "") != update__conf().app_id { continue }
|
||||
if sv_str(a, "PackageId", "") != update__conf(update_st).app_id { continue }
|
||||
let x = new UpdateAsset
|
||||
x.ver = sv_str(a, "Version", "")
|
||||
x.file = sv_str(a, "FileName", "")
|
||||
|
|
@ -31,9 +31,9 @@ function update__assets(feed: Val) -> []UpdateAsset {
|
|||
# What the feed calls the FULL package of version v. Never built here: Velopack puts the channel in
|
||||
# a macOS name (App-0.3.4-osx-full.nupkg) and not a Windows one, and it is both the name a patch
|
||||
# writes and the name the next update looks for as its base.
|
||||
function update__full_name(as: []UpdateAsset, v: string) -> string {
|
||||
function update__full_name(update_st: mut UpdateState, as: []UpdateAsset, v: string) -> string {
|
||||
for i in 0 .. len(as) { if not as[i].delta and as[i].ver == v { return as[i].file } }
|
||||
return update__conf().app_id + "-" + v + "-full.nupkg"
|
||||
return update__conf(update_st).app_id + "-" + v + "-full.nupkg"
|
||||
}
|
||||
|
||||
function update__newest_full(as: []UpdateAsset) -> int {
|
||||
|
|
@ -46,9 +46,9 @@ function update__newest_full(as: []UpdateAsset) -> int {
|
|||
}
|
||||
|
||||
# the deltas above this version up to the newest, oldest first
|
||||
function update__chain(as: []UpdateAsset, newest: string) -> []UpdateAsset {
|
||||
function update__chain(update_st: mut UpdateState, as: []UpdateAsset, newest: string) -> []UpdateAsset {
|
||||
let out = new []UpdateAsset
|
||||
let mine = update__conf().version
|
||||
let mine = update__conf(update_st).version
|
||||
for i in 0 .. len(as) {
|
||||
let a = as[i]
|
||||
if not a.delta or not update_newer(a.ver, mine) or update_newer(a.ver, newest) { continue }
|
||||
|
|
@ -65,18 +65,18 @@ function update__chain(as: []UpdateAsset, newest: string) -> []UpdateAsset {
|
|||
return out
|
||||
}
|
||||
|
||||
function update__fulls_after(as: []UpdateAsset) -> int {
|
||||
function update__fulls_after(update_st: mut UpdateState, as: []UpdateAsset) -> int {
|
||||
var n = 0
|
||||
for i in 0 .. len(as) { if not as[i].delta and update_newer(as[i].ver, update__conf().version) { n += 1 } }
|
||||
for i in 0 .. len(as) { if not as[i].delta and update_newer(as[i].ver, update__conf(update_st).version) { n += 1 } }
|
||||
return n
|
||||
}
|
||||
|
||||
function update__plan_add(a: UpdateAsset, full: string) -> void {
|
||||
push(update__files, a.file)
|
||||
push(update__sha, a.sha)
|
||||
push(update__size, a.size)
|
||||
push(update__delta, a.delta)
|
||||
push(update__ver, a.ver)
|
||||
push(update__full, full)
|
||||
update__total_bytes += a.size
|
||||
function update__plan_add(update_st: mut UpdateState, a: UpdateAsset, full: string) -> void {
|
||||
push(update_st.update__files, a.file)
|
||||
push(update_st.update__sha, a.sha)
|
||||
push(update_st.update__size, a.size)
|
||||
push(update_st.update__delta, a.delta)
|
||||
push(update_st.update__ver, a.ver)
|
||||
push(update_st.update__full, full)
|
||||
update_st.update__total_bytes += a.size
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,70 @@
|
|||
# queries.ludic - what a panel draws: the step, the bar, the pace, and why it stopped
|
||||
export function update_state() -> int { return update__state }
|
||||
export function update_error() -> int { return update__err }
|
||||
export function update_busy() -> bool { return update__state != UPDATE_IDLE and update__state != UPDATE_FAILED }
|
||||
export function update_failed() -> bool { return update__state == UPDATE_FAILED }
|
||||
export function update_state(update_st: UpdateState) -> int { return update_st.update__state }
|
||||
export function update_error(update_st: UpdateState) -> int { return update_st.update__err }
|
||||
export function update_busy(update_st: UpdateState) -> bool { return update_st.update__state != UPDATE_IDLE and update_st.update__state != UPDATE_FAILED }
|
||||
export function update_failed(update_st: UpdateState) -> bool { return update_st.update__state == UPDATE_FAILED }
|
||||
|
||||
# checking and patching have no fraction to give: a panel's bar sweeps instead of filling
|
||||
export function update_working() -> bool { return update__state == UPDATE_CHECK or update__state == UPDATE_PATCH }
|
||||
export function update_working(update_st: UpdateState) -> bool { return update_st.update__state == UPDATE_CHECK or update_st.update__state == UPDATE_PATCH }
|
||||
|
||||
# the package being fetched, checked or patched (0-based), its name, size and version
|
||||
export function update_index() -> int { return update__i }
|
||||
export function update_index(update_st: UpdateState) -> int { return update_st.update__i }
|
||||
|
||||
export function update_file() -> string {
|
||||
if update__files == null or update__i >= len(update__files) { return "" }
|
||||
return update__files[update__i]
|
||||
export function update_file(update_st: UpdateState) -> string {
|
||||
if update_st.update__files == null or update_st.update__i >= len(update_st.update__files) { return "" }
|
||||
return update_st.update__files[update_st.update__i]
|
||||
}
|
||||
|
||||
export function update_file_size() -> int {
|
||||
if update__size == null or update__i >= len(update__size) { return 0 }
|
||||
return update__size[update__i]
|
||||
export function update_file_size(update_st: UpdateState) -> int {
|
||||
if update_st.update__size == null or update_st.update__i >= len(update_st.update__size) { return 0 }
|
||||
return update_st.update__size[update_st.update__i]
|
||||
}
|
||||
|
||||
export function update_version_at() -> string {
|
||||
if update__ver == null or update__i >= len(update__ver) { return "" }
|
||||
return update__ver[update__i]
|
||||
export function update_version_at(update_st: UpdateState) -> string {
|
||||
if update_st.update__ver == null or update_st.update__i >= len(update_st.update__ver) { return "" }
|
||||
return update_st.update__ver[update_st.update__i]
|
||||
}
|
||||
|
||||
# bytes of the package in flight, and of the whole plan
|
||||
export function update_file_received() -> int {
|
||||
if update__state != UPDATE_FETCH or update__h < 0 { return 0 }
|
||||
return UpdateNet.received(update__h)
|
||||
export function update_file_received(update_st: UpdateState) -> int {
|
||||
if update_st.update__state != UPDATE_FETCH or update_st.update__h < 0 { return 0 }
|
||||
return UpdateNet.received(update_st.update__h)
|
||||
}
|
||||
|
||||
export function update_received() -> int { return update__done_bytes + update_file_received() }
|
||||
export function update_received(update_st: UpdateState) -> int { return update_st.update__done_bytes + update_file_received(update_st) }
|
||||
|
||||
# how far, 0..1000, over every byte of the plan (the file being checked counts as fetched)
|
||||
export function update_progress() -> int {
|
||||
if update__total_bytes <= 0 { return 0 }
|
||||
var got = update__done_bytes
|
||||
if update__state == UPDATE_FETCH { got = update_received() }
|
||||
if update__state == UPDATE_CHECK { got = update__done_bytes + update__size[update__i] }
|
||||
return min(1000, got / max(1, update__total_bytes / 1000))
|
||||
export function update_progress(update_st: UpdateState) -> int {
|
||||
if update_st.update__total_bytes <= 0 { return 0 }
|
||||
var got = update_st.update__done_bytes
|
||||
if update_st.update__state == UPDATE_FETCH { got = update_received(update_st) }
|
||||
if update_st.update__state == UPDATE_CHECK { got = update_st.update__done_bytes + update_st.update__size[update_st.update__i] }
|
||||
return min(1000, got / max(1, update_st.update__total_bytes / 1000))
|
||||
}
|
||||
|
||||
# seconds a check or a patch has been running
|
||||
export function update_step_seconds() -> int { return (UpdateWorld.now_ms() - update__step_t0) / 1000 }
|
||||
export function update_step_seconds(update_st: UpdateState) -> int { return (UpdateWorld.now_ms() - update_st.update__step_t0) / 1000 }
|
||||
|
||||
# seconds left at the eased pace; -1 until the pace is known
|
||||
export function update_eta_seconds() -> int {
|
||||
if update__state != UPDATE_FETCH or update__rate <= 0 { return -1 }
|
||||
return (update__total_bytes - update_received()) / update__rate + 1
|
||||
export function update_eta_seconds(update_st: UpdateState) -> int {
|
||||
if update_st.update__state != UPDATE_FETCH or update_st.update__rate <= 0 { return -1 }
|
||||
return (update_st.update__total_bytes - update_received(update_st)) / update_st.update__rate + 1
|
||||
}
|
||||
|
||||
# every half second: the pace, eased so the estimate does not jump
|
||||
function update__rate_tick() -> void {
|
||||
if update__state != UPDATE_FETCH { return }
|
||||
function update__rate_tick(update_st: mut UpdateState) -> void {
|
||||
if update_st.update__state != UPDATE_FETCH { return }
|
||||
let now = UpdateWorld.now_ms()
|
||||
let got = update_received()
|
||||
if update__rate_t < 0 {
|
||||
update__rate_t = now
|
||||
update__rate_bytes = got
|
||||
let got = update_received(update_st)
|
||||
if update_st.update__rate_t < 0 {
|
||||
update_st.update__rate_t = now
|
||||
update_st.update__rate_bytes = got
|
||||
return
|
||||
}
|
||||
let ms = now - update__rate_t
|
||||
let ms = now - update_st.update__rate_t
|
||||
if ms < 500 { return }
|
||||
let per_s = (got - update__rate_bytes) / ms * 1000
|
||||
if update__rate == 0 { update__rate = per_s } else { update__rate = (update__rate * 3 + per_s) / 4 }
|
||||
update__rate_t = now
|
||||
update__rate_bytes = got
|
||||
let per_s = (got - update_st.update__rate_bytes) / ms * 1000
|
||||
if update_st.update__rate == 0 { update_st.update__rate = per_s } else { update_st.update__rate = (update_st.update__rate * 3 + per_s) / 4 }
|
||||
update_st.update__rate_t = now
|
||||
update_st.update__rate_bytes = got
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,47 +51,53 @@ export property UpdateFact {
|
|||
bytes: int = 0
|
||||
}
|
||||
|
||||
var update__cfg: UpdateConfig = null
|
||||
var update__state: int = 0
|
||||
var update__err: int = 0
|
||||
var update__h: int = -1 # the request
|
||||
var update__proc: int = -1 # the child process
|
||||
var update__target: string = ""
|
||||
var update__files: []string = null # the plan: file names, hashes, sizes, delta or not, versions
|
||||
var update__sha: []string = null
|
||||
var update__size: []int = null
|
||||
var update__delta: []bool = null
|
||||
var update__ver: []string = null
|
||||
var update__full: []string = null # what the feed calls the FULL package of that version
|
||||
var update__i: int = 0
|
||||
var update__done_bytes: int = 0
|
||||
var update__total_bytes: int = 0
|
||||
var update__old: string = "" # the full package the next delta patches
|
||||
var update__package: string = "" # what apply installs
|
||||
var update__step_t0: int = 0 # when a check or a patch began, ms
|
||||
var update__rate: int = 0 # bytes a second, eased
|
||||
var update__rate_t: int = -1
|
||||
var update__rate_bytes: int = 0
|
||||
var update__facts: Queue<UpdateFact> = null
|
||||
|
||||
export function update_config(c: UpdateConfig) -> void { update__cfg = c }
|
||||
|
||||
function update__conf() -> UpdateConfig {
|
||||
if update__cfg == null { update__cfg = new UpdateConfig }
|
||||
return update__cfg
|
||||
export state UpdateState {
|
||||
update__ask: int = 0
|
||||
update__ask_h: int = -1
|
||||
update__latest: string = ""
|
||||
update__notes: Val = null
|
||||
update__cfg: UpdateConfig = null
|
||||
update__state: int = 0
|
||||
update__err: int = 0
|
||||
update__h: int = -1 # the request
|
||||
update__proc: int = -1 # the child process
|
||||
update__target: string = ""
|
||||
update__files: []string = null # the plan: file names, hashes, sizes, delta or not, versions
|
||||
update__sha: []string = null
|
||||
update__size: []int = null
|
||||
update__delta: []bool = null
|
||||
update__ver: []string = null
|
||||
update__full: []string = null # what the feed calls the FULL package of that version
|
||||
update__i: int = 0
|
||||
update__done_bytes: int = 0
|
||||
update__total_bytes: int = 0
|
||||
update__old: string = "" # the full package the next delta patches
|
||||
update__package: string = "" # what apply installs
|
||||
update__step_t0: int = 0 # when a check or a patch began, ms
|
||||
update__rate: int = 0 # bytes a second, eased
|
||||
update__rate_t: int = -1
|
||||
update__rate_bytes: int = 0
|
||||
update__facts: Queue<UpdateFact> = null
|
||||
}
|
||||
|
||||
export function update_facts() -> Queue<UpdateFact> {
|
||||
if update__facts == null { update__facts = queue_new("update.facts") }
|
||||
return update__facts
|
||||
export function update_config(update_st: mut UpdateState, c: UpdateConfig) -> void { update_st.update__cfg = c }
|
||||
|
||||
function update__conf(update_st: mut UpdateState) -> UpdateConfig {
|
||||
if update_st.update__cfg == null { update_st.update__cfg = new UpdateConfig }
|
||||
return update_st.update__cfg
|
||||
}
|
||||
|
||||
function update__fact(what: int) -> void {
|
||||
export function update_facts(base_st: mut BaseState, update_st: mut UpdateState) -> Queue<UpdateFact> {
|
||||
if update_st.update__facts == null { update_st.update__facts = queue_new(base_st, "update.facts") }
|
||||
return update_st.update__facts
|
||||
}
|
||||
|
||||
function update__fact(base_st: mut BaseState, update_st: mut UpdateState, what: int) -> void {
|
||||
let f = new UpdateFact
|
||||
f.what = what
|
||||
f.error = update__err
|
||||
f.target = update__target
|
||||
f.packages = update_package_count()
|
||||
f.bytes = update__total_bytes
|
||||
q_push(update_facts(), f)
|
||||
f.error = update_st.update__err
|
||||
f.target = update_st.update__target
|
||||
f.packages = update_package_count(update_st)
|
||||
f.bytes = update_st.update__total_bytes
|
||||
q_push(base_st, update_facts(base_st, update_st), f)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,87 @@
|
|||
# steps.ludic - each step started: the feed asked, a package streamed into the packages folder, its
|
||||
# SHA-256 by a child (Get-FileHash, shasum) so the frame keeps drawing, a patch, the apply
|
||||
# the Update button: false (and nothing started) when Velopack did not install this copy
|
||||
export function update_start() -> bool {
|
||||
if not update_installed() {
|
||||
update__err = UPDATE_ERR_NOT_INSTALLED
|
||||
export function update_start(base_st: mut BaseState, update_st: mut UpdateState) -> bool {
|
||||
if not update_installed(update_st) {
|
||||
update_st.update__err = UPDATE_ERR_NOT_INSTALLED
|
||||
return false
|
||||
}
|
||||
update__release()
|
||||
Fs.mkdir(update_packages())
|
||||
update__err = UPDATE_ERR_NONE
|
||||
update__h = UpdateNet.get(update__conf().feed + "/" + update__feed_file())
|
||||
if update__h < 0 {
|
||||
update__fail(UPDATE_ERR_UNREACHABLE)
|
||||
update__release(update_st)
|
||||
Fs.mkdir(update_packages(update_st))
|
||||
update_st.update__err = UPDATE_ERR_NONE
|
||||
update_st.update__h = UpdateNet.get(update__conf(update_st).feed + "/" + update__feed_file())
|
||||
if update_st.update__h < 0 {
|
||||
update__fail(base_st, update_st, UPDATE_ERR_UNREACHABLE)
|
||||
return true
|
||||
}
|
||||
update__state = UPDATE_FEED
|
||||
update_st.update__state = UPDATE_FEED
|
||||
return true
|
||||
}
|
||||
|
||||
function update__release() -> void {
|
||||
if update__h >= 0 { UpdateNet.free(update__h) }
|
||||
update__h = -1
|
||||
if update__proc >= 0 { UpdateProc.free(update__proc) }
|
||||
update__proc = -1
|
||||
function update__release(update_st: mut UpdateState) -> void {
|
||||
if update_st.update__h >= 0 { UpdateNet.free(update_st.update__h) }
|
||||
update_st.update__h = -1
|
||||
if update_st.update__proc >= 0 { UpdateProc.free(update_st.update__proc) }
|
||||
update_st.update__proc = -1
|
||||
}
|
||||
|
||||
function update__fail(err: int) -> void {
|
||||
update__release()
|
||||
update__err = err
|
||||
update__state = UPDATE_FAILED
|
||||
update__fact(UPDATE_STOPPED)
|
||||
function update__fail(base_st: mut BaseState, update_st: mut UpdateState, err: int) -> void {
|
||||
update__release(update_st)
|
||||
update_st.update__err = err
|
||||
update_st.update__state = UPDATE_FAILED
|
||||
update__fact(base_st, update_st, UPDATE_STOPPED)
|
||||
}
|
||||
|
||||
# fetch file i, unless a copy of the right size is there already - then straight to its check
|
||||
function update__fetch(i: int) -> void {
|
||||
update__i = i
|
||||
let p = update__path(i)
|
||||
if Fs.exists(p) and Fs.size(p) == update__size[i] {
|
||||
update__check(i)
|
||||
function update__fetch(base_st: mut BaseState, update_st: mut UpdateState, i: int) -> void {
|
||||
update_st.update__i = i
|
||||
let p = update__path(update_st, i)
|
||||
if Fs.exists(p) and Fs.size(p) == update_st.update__size[i] {
|
||||
update__check(base_st, update_st, i)
|
||||
return
|
||||
}
|
||||
if Fs.exists(p) { Fs.remove(p) }
|
||||
update__h = UpdateNet.download(update__conf().feed + "/" + update__files[i], p)
|
||||
if update__h < 0 {
|
||||
update__fail(UPDATE_ERR_UNREACHABLE)
|
||||
update_st.update__h = UpdateNet.download(update__conf(update_st).feed + "/" + update_st.update__files[i], p)
|
||||
if update_st.update__h < 0 {
|
||||
update__fail(base_st, update_st, UPDATE_ERR_UNREACHABLE)
|
||||
return
|
||||
}
|
||||
update__rate_t = -1
|
||||
update__state = UPDATE_FETCH
|
||||
update_st.update__rate_t = -1
|
||||
update_st.update__state = UPDATE_FETCH
|
||||
}
|
||||
|
||||
function update__hash_file() -> string {
|
||||
if len(update__conf().scratch) > 0 { return update__conf().scratch }
|
||||
function update__hash_file(update_st: mut UpdateState) -> string {
|
||||
if len(update__conf(update_st).scratch) > 0 { return update__conf(update_st).scratch }
|
||||
return Os.temp_dir() + "/ludic-update-hash.txt"
|
||||
}
|
||||
|
||||
# no cmd.exe: it parses its own command line and would not survive Process.spawn's quoting
|
||||
function update__check(i: int) -> void {
|
||||
let out = update__hash_file()
|
||||
function update__check(base_st: mut BaseState, update_st: mut UpdateState, i: int) -> void {
|
||||
let out = update__hash_file(update_st)
|
||||
if Fs.exists(out) { Fs.remove(out) }
|
||||
let args = new []string
|
||||
if UpdateWorld.platform() == "windows" {
|
||||
push(args, "-NoProfile")
|
||||
push(args, "-NonInteractive")
|
||||
push(args, "-Command")
|
||||
push(args, "(Get-FileHash -Algorithm SHA256 -LiteralPath " + update__psq(update__path(i)) + ").Hash | Out-File -Encoding ascii -LiteralPath " + update__psq(out))
|
||||
update__proc = UpdateProc.spawn(update__powershell(), args)
|
||||
push(args, "(Get-FileHash -Algorithm SHA256 -LiteralPath " + update__psq(update__path(update_st, i)) + ").Hash | Out-File -Encoding ascii -LiteralPath " + update__psq(out))
|
||||
update_st.update__proc = UpdateProc.spawn(update__powershell(), args)
|
||||
} else {
|
||||
push(args, "-c")
|
||||
push(args, "shasum -a 256 '" + update__path(i) + "' | cut -d' ' -f1 > '" + out + "'")
|
||||
update__proc = UpdateProc.spawn("/bin/sh", args)
|
||||
push(args, "shasum -a 256 '" + update__path(update_st, i) + "' | cut -d' ' -f1 > '" + out + "'")
|
||||
update_st.update__proc = UpdateProc.spawn("/bin/sh", args)
|
||||
}
|
||||
if update__proc < 0 {
|
||||
update__fail(UPDATE_ERR_CHECK)
|
||||
if update_st.update__proc < 0 {
|
||||
update__fail(base_st, update_st, UPDATE_ERR_CHECK)
|
||||
return
|
||||
}
|
||||
update__state = UPDATE_CHECK
|
||||
update__step_t0 = UpdateWorld.now_ms()
|
||||
update_st.update__state = UPDATE_CHECK
|
||||
update_st.update__step_t0 = UpdateWorld.now_ms()
|
||||
}
|
||||
|
||||
# the hash the child wrote: the first line with anything on it
|
||||
function update__hash_read() -> string {
|
||||
let s = Fs.read_text(update__hash_file())
|
||||
function update__hash_read(update_st: mut UpdateState) -> string {
|
||||
let s = Fs.read_text(update__hash_file(update_st))
|
||||
if s == null { return "" }
|
||||
let lines = Text.split(s, "\n")
|
||||
for i in 0 .. len(lines) {
|
||||
|
|
|
|||
|
|
@ -4,40 +4,42 @@ import "ludic.update"
|
|||
import "ludic.base"
|
||||
program UpdateTest {
|
||||
numbers float
|
||||
var net_answer: int = -1
|
||||
var net_text: string = ""
|
||||
var net_urls: []string = null
|
||||
var net_paths: []string = null
|
||||
var net_got: int = 0
|
||||
var proc_code: int = -1
|
||||
var proc_paths: []string = null
|
||||
var proc_args: []string = null
|
||||
var exe: string = ""
|
||||
state UpdatetestState {
|
||||
net_answer: int = -1
|
||||
net_text: string = ""
|
||||
net_urls: []string = null
|
||||
net_paths: []string = null
|
||||
net_got: int = 0
|
||||
proc_code: int = -1
|
||||
proc_paths: []string = null
|
||||
proc_args: []string = null
|
||||
exe: string = ""
|
||||
}
|
||||
|
||||
function fake_platform() -> string { return "macos" }
|
||||
function fake_exe() -> string { return exe }
|
||||
function fake_exe(updatetest_st: UpdatetestState) -> string { return updatetest_st.exe }
|
||||
function fake_pid() -> int { return 4242 }
|
||||
function fake_ms() -> int { return 0 }
|
||||
function fake_get(url: string) -> int {
|
||||
push(net_urls, url)
|
||||
push(net_paths, "")
|
||||
return len(net_urls) - 1
|
||||
function fake_get(updatetest_st: mut UpdatetestState, url: string) -> int {
|
||||
push(updatetest_st.net_urls, url)
|
||||
push(updatetest_st.net_paths, "")
|
||||
return len(updatetest_st.net_urls) - 1
|
||||
}
|
||||
function fake_download(url: string, path: string) -> int {
|
||||
push(net_urls, url)
|
||||
push(net_paths, path)
|
||||
return len(net_urls) - 1
|
||||
function fake_download(updatetest_st: mut UpdatetestState, url: string, path: string) -> int {
|
||||
push(updatetest_st.net_urls, url)
|
||||
push(updatetest_st.net_paths, path)
|
||||
return len(updatetest_st.net_urls) - 1
|
||||
}
|
||||
function fake_poll(h: int) -> int { return net_answer }
|
||||
function fake_text(h: int) -> string { return net_text }
|
||||
function fake_received(h: int) -> int { return net_got }
|
||||
function fake_poll(updatetest_st: UpdatetestState, h: int) -> int { return updatetest_st.net_answer }
|
||||
function fake_text(updatetest_st: UpdatetestState, h: int) -> string { return updatetest_st.net_text }
|
||||
function fake_received(updatetest_st: UpdatetestState, h: int) -> int { return updatetest_st.net_got }
|
||||
function fake_free(h: int) -> void { }
|
||||
function fake_spawn(path: string, args: []string) -> int {
|
||||
push(proc_paths, path)
|
||||
push(proc_args, Text.join(args, " "))
|
||||
return len(proc_paths) - 1
|
||||
function fake_spawn(updatetest_st: mut UpdatetestState, path: string, args: []string) -> int {
|
||||
push(updatetest_st.proc_paths, path)
|
||||
push(updatetest_st.proc_args, Text.join(args, " "))
|
||||
return len(updatetest_st.proc_paths) - 1
|
||||
}
|
||||
function fake_proc_poll(h: int) -> int { return proc_code }
|
||||
function fake_proc_poll(updatetest_st: UpdatetestState, h: int) -> int { return updatetest_st.proc_code }
|
||||
function fake_proc_free(h: int) -> void { }
|
||||
|
||||
bind UpdateWorld { platform: fn fake_platform, exe: fn fake_exe, pid: fn fake_pid, now_ms: fn fake_ms }
|
||||
|
|
@ -48,16 +50,16 @@ program UpdateTest {
|
|||
function pkgs() -> string { return dir() + "/packages" }
|
||||
function hashf() -> string { return dir() + "/hash.txt" }
|
||||
|
||||
function fresh(root: bool) -> void {
|
||||
net_answer = -1
|
||||
net_text = ""
|
||||
net_urls = new []string
|
||||
net_paths = new []string
|
||||
net_got = 0
|
||||
proc_code = -1
|
||||
proc_paths = new []string
|
||||
proc_args = new []string
|
||||
exe = dir() + "/Game.app/Contents/MacOS/Game"
|
||||
function fresh(base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState, root: bool) -> void {
|
||||
updatetest_st.net_answer = -1
|
||||
updatetest_st.net_text = ""
|
||||
updatetest_st.net_urls = new []string
|
||||
updatetest_st.net_paths = new []string
|
||||
updatetest_st.net_got = 0
|
||||
updatetest_st.proc_code = -1
|
||||
updatetest_st.proc_paths = new []string
|
||||
updatetest_st.proc_args = new []string
|
||||
updatetest_st.exe = dir() + "/Game.app/Contents/MacOS/Game"
|
||||
System.run("rm -rf '" + dir() + "'")
|
||||
Fs.mkdir(dir())
|
||||
Fs.mkdir(pkgs())
|
||||
|
|
@ -68,8 +70,8 @@ program UpdateTest {
|
|||
if root { c.root = dir() + "/Game.app" }
|
||||
c.packages = pkgs()
|
||||
c.scratch = hashf()
|
||||
update_config(c)
|
||||
update_reset()
|
||||
update_config(update_st, c)
|
||||
update_reset(base_st, update_st)
|
||||
}
|
||||
|
||||
function asset(ty: string, ver: string, size: int) -> string {
|
||||
|
|
@ -85,18 +87,18 @@ program UpdateTest {
|
|||
}
|
||||
|
||||
function seed_base() -> void { Fs.write_text(pkgs() + "/Game-1.0.0-osx-full.nupkg", "base") }
|
||||
function plan_of(text: string) -> bool { return update_plan(Json.parse(text)) }
|
||||
function plan_of(update_st: mut UpdateState, text: string) -> bool { return update_plan(update_st, Json.parse(text)) }
|
||||
|
||||
function facts_of(what: int) -> int {
|
||||
let fs = q_drain(update_facts())
|
||||
function facts_of(base_st: mut BaseState, update_st: mut UpdateState, what: int) -> int {
|
||||
let fs = q_drain(base_st, update_facts(base_st, update_st))
|
||||
var n = 0
|
||||
for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } }
|
||||
return n
|
||||
}
|
||||
|
||||
function hash_is(h: string) -> void { Fs.write_text(hashf(), h + "\n") }
|
||||
function last_args() -> string { return proc_args[len(proc_args) - 1] }
|
||||
function last_url() -> string { return net_urls[len(net_urls) - 1] }
|
||||
function last_args(updatetest_st: UpdatetestState) -> string { return updatetest_st.proc_args[len(updatetest_st.proc_args) - 1] }
|
||||
function last_url(updatetest_st: UpdatetestState) -> string { return updatetest_st.net_urls[len(updatetest_st.net_urls) - 1] }
|
||||
|
||||
test "versions compare a number at a time" {
|
||||
expect(update_newer("0.10.2", "0.9.9"))
|
||||
|
|
@ -105,166 +107,166 @@ program UpdateTest {
|
|||
expect(not update_newer("0.4.9", "0.5.0"))
|
||||
}
|
||||
|
||||
test "a newer version is shown with its notes, in the language asked for or English" {
|
||||
fresh(true)
|
||||
update_check_tick(false)
|
||||
expect(last_url() == "http://feed/latest.json")
|
||||
net_text = "{\"version\":\"1.2.0\",\"notes\":{\"en\":[\"Fish.\",\"Boats.\"],\"tr\":[\"Balik.\"]}}"
|
||||
net_answer = 1
|
||||
update_check_tick(false)
|
||||
expect(update_fresh())
|
||||
expect(update_latest() == "1.2.0")
|
||||
expect_eq(len(update_notes("tr")), 1)
|
||||
expect_eq(len(update_notes("de")), 2)
|
||||
test "a newer version is shown with its notes, in the language asked for or English" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
update_check_tick(update_st, false)
|
||||
expect(last_url(updatetest_st) == "http://feed/latest.json")
|
||||
updatetest_st.net_text = "{\"version\":\"1.2.0\",\"notes\":{\"en\":[\"Fish.\",\"Boats.\"],\"tr\":[\"Balik.\"]}}"
|
||||
updatetest_st.net_answer = 1
|
||||
update_check_tick(update_st, false)
|
||||
expect(update_fresh(update_st))
|
||||
expect(update_latest(update_st) == "1.2.0")
|
||||
expect_eq(len(update_notes(update_st, "tr")), 1)
|
||||
expect_eq(len(update_notes(update_st, "de")), 2)
|
||||
}
|
||||
|
||||
test "the version being played is shown only when asked" {
|
||||
fresh(true)
|
||||
net_text = "{\"version\":\"1.0.0\",\"notes\":{\"en\":[\"x\"]}}"
|
||||
net_answer = 1
|
||||
update_check_tick(false)
|
||||
update_check_tick(false)
|
||||
expect(not update_shown())
|
||||
update_reset()
|
||||
update_check_tick(true)
|
||||
update_check_tick(true)
|
||||
expect_eq(update_ask_state(), UPDATE_CURRENT)
|
||||
update_reset()
|
||||
net_answer = 0
|
||||
update_check_tick(true)
|
||||
update_check_tick(true)
|
||||
expect_eq(update_ask_state(), UPDATE_NONE)
|
||||
test "the version being played is shown only when asked" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
updatetest_st.net_text = "{\"version\":\"1.0.0\",\"notes\":{\"en\":[\"x\"]}}"
|
||||
updatetest_st.net_answer = 1
|
||||
update_check_tick(update_st, false)
|
||||
update_check_tick(update_st, false)
|
||||
expect(not update_shown(update_st))
|
||||
update_reset(base_st, update_st)
|
||||
update_check_tick(update_st, true)
|
||||
update_check_tick(update_st, true)
|
||||
expect_eq(update_ask_state(update_st), UPDATE_CURRENT)
|
||||
update_reset(base_st, update_st)
|
||||
updatetest_st.net_answer = 0
|
||||
update_check_tick(update_st, true)
|
||||
update_check_tick(update_st, true)
|
||||
expect_eq(update_ask_state(update_st), UPDATE_NONE)
|
||||
}
|
||||
|
||||
test "with the base on disk and every delta there, the plan is the chain, oldest first" {
|
||||
fresh(true)
|
||||
test "with the base on disk and every delta there, the plan is the chain, oldest first" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
seed_base()
|
||||
expect(plan_of(feed(true)))
|
||||
expect_eq(update_package_count(), 2)
|
||||
expect(update_plan_is_delta(0))
|
||||
expect(update_plan_version(0) == "1.1.0")
|
||||
expect(update_plan_version(1) == "1.2.0")
|
||||
expect(update_target() == "1.2.0")
|
||||
expect(plan_of(update_st, feed(true)))
|
||||
expect_eq(update_package_count(update_st), 2)
|
||||
expect(update_plan_is_delta(update_st, 0))
|
||||
expect(update_plan_version(update_st, 0) == "1.1.0")
|
||||
expect(update_plan_version(update_st, 1) == "1.2.0")
|
||||
expect(update_target(update_st) == "1.2.0")
|
||||
}
|
||||
|
||||
test "no base, or a missing delta, is the whole newest package" {
|
||||
fresh(true)
|
||||
expect(plan_of(feed(true)))
|
||||
expect_eq(update_package_count(), 1)
|
||||
expect(not update_plan_is_delta(0))
|
||||
expect(update_plan_file(0) == "Game-1.2.0-osx-full.nupkg")
|
||||
test "no base, or a missing delta, is the whole newest package" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
expect(plan_of(update_st, feed(true)))
|
||||
expect_eq(update_package_count(update_st), 1)
|
||||
expect(not update_plan_is_delta(update_st, 0))
|
||||
expect(update_plan_file(update_st, 0) == "Game-1.2.0-osx-full.nupkg")
|
||||
seed_base()
|
||||
expect(plan_of(feed(false)))
|
||||
expect_eq(update_package_count(), 1)
|
||||
expect(plan_of(update_st, feed(false)))
|
||||
expect_eq(update_package_count(update_st), 1)
|
||||
}
|
||||
|
||||
test "nothing newer is no plan" {
|
||||
fresh(true)
|
||||
expect(not plan_of("{\"Assets\":[" + asset("Full", "1.0.0", 900) + "]}"))
|
||||
expect(not plan_of("{}"))
|
||||
test "nothing newer is no plan" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
expect(not plan_of(update_st, "{\"Assets\":[" + asset("Full", "1.0.0", 900) + "]}"))
|
||||
expect(not plan_of(update_st, "{}"))
|
||||
}
|
||||
|
||||
test "a copy Velopack did not install does not start" {
|
||||
fresh(false)
|
||||
expect(not update_installed())
|
||||
expect(not update_start())
|
||||
expect_eq(update_error(), UPDATE_ERR_NOT_INSTALLED)
|
||||
test "a copy Velopack did not install does not start" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, false)
|
||||
expect(not update_installed(update_st))
|
||||
expect(not update_start(base_st, update_st))
|
||||
expect_eq(update_error(update_st), UPDATE_ERR_NOT_INSTALLED)
|
||||
Fs.mkdir(dir() + "/Game.app")
|
||||
Fs.mkdir(dir() + "/Game.app/Contents")
|
||||
Fs.mkdir(dir() + "/Game.app/Contents/MacOS")
|
||||
Fs.write_text(dir() + "/Game.app/Contents/MacOS/UpdateMac", "x")
|
||||
expect(update_installed())
|
||||
exe = "/private/var/folders/x/AppTranslocation/y/Game.app/Contents/MacOS/Game"
|
||||
expect(not update_installed())
|
||||
expect(update_installed(update_st))
|
||||
updatetest_st.exe = "/private/var/folders/x/AppTranslocation/y/Game.app/Contents/MacOS/Game"
|
||||
expect(not update_installed(update_st))
|
||||
}
|
||||
|
||||
test "the whole package: fetched, checked, applied, and the game told to go" {
|
||||
fresh(true)
|
||||
expect(update_start())
|
||||
expect(last_url() == "http://feed/releases.osx.json")
|
||||
net_text = feed(true)
|
||||
net_answer = 1
|
||||
update_tick()
|
||||
expect_eq(update_state(), UPDATE_FETCH)
|
||||
expect_eq(facts_of(UPDATE_STARTED), 1)
|
||||
expect(last_url() == "http://feed/Game-1.2.0-osx-full.nupkg")
|
||||
net_got = 500
|
||||
expect(update_progress() >= 490 and update_progress() <= 510)
|
||||
update_tick()
|
||||
expect_eq(update_state(), UPDATE_CHECK)
|
||||
expect(Text.contains(last_args(), "shasum -a 256"))
|
||||
test "the whole package: fetched, checked, applied, and the game told to go" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
expect(update_start(base_st, update_st))
|
||||
expect(last_url(updatetest_st) == "http://feed/releases.osx.json")
|
||||
updatetest_st.net_text = feed(true)
|
||||
updatetest_st.net_answer = 1
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_state(update_st), UPDATE_FETCH)
|
||||
expect_eq(facts_of(base_st, update_st, UPDATE_STARTED), 1)
|
||||
expect(last_url(updatetest_st) == "http://feed/Game-1.2.0-osx-full.nupkg")
|
||||
updatetest_st.net_got = 500
|
||||
expect(update_progress(update_st) >= 490 and update_progress(update_st) <= 510)
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_state(update_st), UPDATE_CHECK)
|
||||
expect(Text.contains(last_args(updatetest_st), "shasum -a 256"))
|
||||
hash_is("AB1.2.0")
|
||||
proc_code = 0
|
||||
update_tick()
|
||||
expect_eq(update_state(), UPDATE_APPLY)
|
||||
expect(Text.contains(last_args(), "apply --silent --waitPid 4242 --package"))
|
||||
expect(Text.contains(last_args(), "--rootDir"))
|
||||
expect(Text.ends_with(proc_paths[len(proc_paths) - 1], "Game.app/Contents/MacOS/UpdateMac"))
|
||||
expect_eq(facts_of(UPDATE_APPLYING), 1)
|
||||
updatetest_st.proc_code = 0
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_state(update_st), UPDATE_APPLY)
|
||||
expect(Text.contains(last_args(updatetest_st), "apply --silent --waitPid 4242 --package"))
|
||||
expect(Text.contains(last_args(updatetest_st), "--rootDir"))
|
||||
expect(Text.ends_with(updatetest_st.proc_paths[len(updatetest_st.proc_paths) - 1], "Game.app/Contents/MacOS/UpdateMac"))
|
||||
expect_eq(facts_of(base_st, update_st, UPDATE_APPLYING), 1)
|
||||
}
|
||||
|
||||
test "a wrong hash stops it and deletes the file" {
|
||||
fresh(true)
|
||||
update_start()
|
||||
net_text = feed(true)
|
||||
net_answer = 1
|
||||
update_tick()
|
||||
test "a wrong hash stops it and deletes the file" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
update_start(base_st, update_st)
|
||||
updatetest_st.net_text = feed(true)
|
||||
updatetest_st.net_answer = 1
|
||||
update_tick(base_st, update_st)
|
||||
Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", "junk")
|
||||
update_tick()
|
||||
update_tick(base_st, update_st)
|
||||
hash_is("FFFF")
|
||||
proc_code = 0
|
||||
update_tick()
|
||||
expect(update_failed())
|
||||
expect_eq(update_error(), UPDATE_ERR_CORRUPT)
|
||||
updatetest_st.proc_code = 0
|
||||
update_tick(base_st, update_st)
|
||||
expect(update_failed(update_st))
|
||||
expect_eq(update_error(update_st), UPDATE_ERR_CORRUPT)
|
||||
expect(not Fs.exists(pkgs() + "/Game-1.2.0-osx-full.nupkg"))
|
||||
expect_eq(facts_of(UPDATE_STOPPED), 1)
|
||||
expect_eq(facts_of(base_st, update_st, UPDATE_STOPPED), 1)
|
||||
}
|
||||
|
||||
test "a failed download stops it" {
|
||||
fresh(true)
|
||||
update_start()
|
||||
net_text = feed(true)
|
||||
net_answer = 1
|
||||
update_tick()
|
||||
net_answer = 0
|
||||
update_tick()
|
||||
expect_eq(update_error(), UPDATE_ERR_DOWNLOAD)
|
||||
test "a failed download stops it" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
update_start(base_st, update_st)
|
||||
updatetest_st.net_text = feed(true)
|
||||
updatetest_st.net_answer = 1
|
||||
update_tick(base_st, update_st)
|
||||
updatetest_st.net_answer = 0
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_error(update_st), UPDATE_ERR_DOWNLOAD)
|
||||
}
|
||||
|
||||
test "the delta chain: each fetched, checked and patched onto the last, then applied" {
|
||||
fresh(true)
|
||||
test "the delta chain: each fetched, checked and patched onto the last, then applied" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
seed_base()
|
||||
update_start()
|
||||
net_text = feed(true)
|
||||
net_answer = 1
|
||||
update_tick()
|
||||
update_tick()
|
||||
update_start(base_st, update_st)
|
||||
updatetest_st.net_text = feed(true)
|
||||
updatetest_st.net_answer = 1
|
||||
update_tick(base_st, update_st)
|
||||
update_tick(base_st, update_st)
|
||||
hash_is("AB1.1.0")
|
||||
proc_code = 0
|
||||
update_tick()
|
||||
expect_eq(update_state(), UPDATE_PATCH)
|
||||
expect(Text.contains(last_args(), "--old " + pkgs() + "/Game-1.0.0-osx-full.nupkg"))
|
||||
expect(Text.contains(last_args(), "--output " + pkgs() + "/Game-1.1.0-osx-full.nupkg"))
|
||||
updatetest_st.proc_code = 0
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_state(update_st), UPDATE_PATCH)
|
||||
expect(Text.contains(last_args(updatetest_st), "--old " + pkgs() + "/Game-1.0.0-osx-full.nupkg"))
|
||||
expect(Text.contains(last_args(updatetest_st), "--output " + pkgs() + "/Game-1.1.0-osx-full.nupkg"))
|
||||
Fs.write_text(pkgs() + "/Game-1.1.0-osx-full.nupkg", "patched")
|
||||
update_tick()
|
||||
expect_eq(update_state(), UPDATE_FETCH)
|
||||
expect_eq(update_index(), 1)
|
||||
update_tick()
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_state(update_st), UPDATE_FETCH)
|
||||
expect_eq(update_index(update_st), 1)
|
||||
update_tick(base_st, update_st)
|
||||
hash_is("AB1.2.0")
|
||||
update_tick()
|
||||
update_tick(base_st, update_st)
|
||||
Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", "patched")
|
||||
update_tick()
|
||||
expect_eq(update_state(), UPDATE_APPLY)
|
||||
expect(Text.contains(last_args(), "--package " + pkgs() + "/Game-1.2.0-osx-full.nupkg"))
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_state(update_st), UPDATE_APPLY)
|
||||
expect(Text.contains(last_args(updatetest_st), "--package " + pkgs() + "/Game-1.2.0-osx-full.nupkg"))
|
||||
}
|
||||
|
||||
test "a package already on disk at its size is checked, not fetched again" {
|
||||
fresh(true)
|
||||
update_start()
|
||||
net_text = feed(true)
|
||||
net_answer = 1
|
||||
test "a package already on disk at its size is checked, not fetched again" (base_st: mut BaseState, update_st: mut UpdateState, updatetest_st: mut UpdatetestState) {
|
||||
fresh(base_st, update_st, updatetest_st, true)
|
||||
update_start(base_st, update_st)
|
||||
updatetest_st.net_text = feed(true)
|
||||
updatetest_st.net_answer = 1
|
||||
Fs.write_text(pkgs() + "/Game-1.2.0-osx-full.nupkg", Text.repeat("x", 1000))
|
||||
update_tick()
|
||||
expect_eq(update_state(), UPDATE_CHECK)
|
||||
update_tick(base_st, update_st)
|
||||
expect_eq(update_state(update_st), UPDATE_CHECK)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +1,99 @@
|
|||
# tick.ludic - once a frame while an update runs: whatever the step waits on, and the next step
|
||||
export function update_tick() -> void {
|
||||
update__rate_tick()
|
||||
if update__state == UPDATE_FEED { update__feed_tick() }
|
||||
else if update__state == UPDATE_FETCH { update__fetch_tick() }
|
||||
else if update__state == UPDATE_CHECK { update__check_tick() }
|
||||
else if update__state == UPDATE_PATCH { update__patch_tick() }
|
||||
export function update_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void {
|
||||
update__rate_tick(update_st)
|
||||
if update_st.update__state == UPDATE_FEED { update__feed_tick(base_st, update_st) }
|
||||
else if update_st.update__state == UPDATE_FETCH { update__fetch_tick(base_st, update_st) }
|
||||
else if update_st.update__state == UPDATE_CHECK { update__check_tick(base_st, update_st) }
|
||||
else if update_st.update__state == UPDATE_PATCH { update__patch_tick(base_st, update_st) }
|
||||
}
|
||||
|
||||
function update__feed_tick() -> void {
|
||||
let st = UpdateNet.poll(update__h)
|
||||
function update__feed_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void {
|
||||
let st = UpdateNet.poll(update_st.update__h)
|
||||
if st < 0 { return }
|
||||
var feed: Val = null
|
||||
if st > 0 { feed = Json.parse(UpdateNet.text(update__h)) }
|
||||
UpdateNet.free(update__h)
|
||||
update__h = -1
|
||||
if st > 0 { feed = Json.parse(UpdateNet.text(update_st.update__h)) }
|
||||
UpdateNet.free(update_st.update__h)
|
||||
update_st.update__h = -1
|
||||
if feed == null {
|
||||
update__fail(UPDATE_ERR_UNREACHABLE)
|
||||
update__fail(base_st, update_st, UPDATE_ERR_UNREACHABLE)
|
||||
return
|
||||
}
|
||||
if not update_plan(feed) {
|
||||
update__fail(UPDATE_ERR_NOTHING)
|
||||
if not update_plan(update_st, feed) {
|
||||
update__fail(base_st, update_st, UPDATE_ERR_NOTHING)
|
||||
return
|
||||
}
|
||||
update__fact(UPDATE_STARTED)
|
||||
update__fetch(0)
|
||||
update__fact(base_st, update_st, UPDATE_STARTED)
|
||||
update__fetch(base_st, update_st, 0)
|
||||
}
|
||||
|
||||
function update__fetch_tick() -> void {
|
||||
let st = UpdateNet.poll(update__h)
|
||||
function update__fetch_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void {
|
||||
let st = UpdateNet.poll(update_st.update__h)
|
||||
if st < 0 { return }
|
||||
UpdateNet.free(update__h)
|
||||
update__h = -1
|
||||
UpdateNet.free(update_st.update__h)
|
||||
update_st.update__h = -1
|
||||
if st == 0 {
|
||||
Fs.remove(update__path(update__i))
|
||||
update__fail(UPDATE_ERR_DOWNLOAD)
|
||||
Fs.remove(update__path(update_st, update_st.update__i))
|
||||
update__fail(base_st, update_st, UPDATE_ERR_DOWNLOAD)
|
||||
return
|
||||
}
|
||||
update__check(update__i)
|
||||
update__check(base_st, update_st, update_st.update__i)
|
||||
}
|
||||
|
||||
# a package rebuilt from deltas is not compared with the full one's hash: patch re-assembles the
|
||||
# archive, so the same files come out as different bytes
|
||||
function update__check_tick() -> void {
|
||||
let code = UpdateProc.poll(update__proc)
|
||||
function update__check_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void {
|
||||
let code = UpdateProc.poll(update_st.update__proc)
|
||||
if code < 0 { return }
|
||||
UpdateProc.free(update__proc)
|
||||
update__proc = -1
|
||||
if code != 0 or update__hash_read() != Text.upper(update__sha[update__i]) {
|
||||
Fs.remove(update__path(update__i))
|
||||
update__fail(UPDATE_ERR_CORRUPT)
|
||||
UpdateProc.free(update_st.update__proc)
|
||||
update_st.update__proc = -1
|
||||
if code != 0 or update__hash_read(update_st) != Text.upper(update_st.update__sha[update_st.update__i]) {
|
||||
Fs.remove(update__path(update_st, update_st.update__i))
|
||||
update__fail(base_st, update_st, UPDATE_ERR_CORRUPT)
|
||||
return
|
||||
}
|
||||
update__done_bytes += update__size[update__i]
|
||||
if update__delta[update__i] { update__patch(update__i) } else { update__apply(update__path(update__i)) }
|
||||
update_st.update__done_bytes += update_st.update__size[update_st.update__i]
|
||||
if update_st.update__delta[update_st.update__i] { update__patch(base_st, update_st, update_st.update__i) } else { update__apply(base_st, update_st, update__path(update_st, update_st.update__i)) }
|
||||
}
|
||||
|
||||
function update__patch(i: int) -> void {
|
||||
let out = update_packages() + update__sep() + update__full[i]
|
||||
function update__patch(base_st: mut BaseState, update_st: mut UpdateState, i: int) -> void {
|
||||
let out = update_packages(update_st) + update__sep() + update_st.update__full[i]
|
||||
if Fs.exists(out) { Fs.remove(out) }
|
||||
let args = new []string
|
||||
push(args, "patch")
|
||||
push(args, "--silent")
|
||||
push(args, "--old")
|
||||
push(args, update__old)
|
||||
push(args, update_st.update__old)
|
||||
push(args, "--delta")
|
||||
push(args, update__path(i))
|
||||
push(args, update__path(update_st, i))
|
||||
push(args, "--output")
|
||||
push(args, out)
|
||||
update__proc = UpdateProc.spawn(update__updater(), args)
|
||||
if update__proc < 0 {
|
||||
update__fail(UPDATE_ERR_PATCH_START)
|
||||
update_st.update__proc = UpdateProc.spawn(update__updater(update_st), args)
|
||||
if update_st.update__proc < 0 {
|
||||
update__fail(base_st, update_st, UPDATE_ERR_PATCH_START)
|
||||
return
|
||||
}
|
||||
update__old = out
|
||||
update__state = UPDATE_PATCH
|
||||
update__step_t0 = UpdateWorld.now_ms()
|
||||
update_st.update__old = out
|
||||
update_st.update__state = UPDATE_PATCH
|
||||
update_st.update__step_t0 = UpdateWorld.now_ms()
|
||||
}
|
||||
|
||||
function update__patch_tick() -> void {
|
||||
let code = UpdateProc.poll(update__proc)
|
||||
function update__patch_tick(base_st: mut BaseState, update_st: mut UpdateState) -> void {
|
||||
let code = UpdateProc.poll(update_st.update__proc)
|
||||
if code < 0 { return }
|
||||
UpdateProc.free(update__proc)
|
||||
update__proc = -1
|
||||
if code != 0 or not Fs.exists(update__old) {
|
||||
update__fail(UPDATE_ERR_PATCH)
|
||||
UpdateProc.free(update_st.update__proc)
|
||||
update_st.update__proc = -1
|
||||
if code != 0 or not Fs.exists(update_st.update__old) {
|
||||
update__fail(base_st, update_st, UPDATE_ERR_PATCH)
|
||||
return
|
||||
}
|
||||
if update__i + 1 < len(update__files) { update__fetch(update__i + 1) } else { update__apply(update__old) }
|
||||
if update_st.update__i + 1 < len(update_st.update__files) { update__fetch(base_st, update_st, update_st.update__i + 1) } else { update__apply(base_st, update_st, update_st.update__old) }
|
||||
}
|
||||
|
||||
# Velopack waits for this process to be gone, swaps the install and starts the new version; the
|
||||
# game hears UPDATE_APPLYING and exits. On macOS neither the root nor the packages are where
|
||||
# UpdateMac would look, so both are named.
|
||||
function update__apply(package: string) -> void {
|
||||
update__package = package
|
||||
function update__apply(base_st: mut BaseState, update_st: mut UpdateState, package: string) -> void {
|
||||
update_st.update__package = package
|
||||
let args = new []string
|
||||
push(args, "apply")
|
||||
push(args, "--silent")
|
||||
|
|
@ -103,15 +103,15 @@ function update__apply(package: string) -> void {
|
|||
push(args, package)
|
||||
if update__mac() {
|
||||
push(args, "--rootDir")
|
||||
push(args, update_root())
|
||||
push(args, update_root(update_st))
|
||||
push(args, "--packageDir")
|
||||
push(args, update_packages())
|
||||
push(args, update_packages(update_st))
|
||||
}
|
||||
update__proc = UpdateProc.spawn(update__updater(), args)
|
||||
if update__proc < 0 {
|
||||
update__fail(UPDATE_ERR_APPLY)
|
||||
update_st.update__proc = UpdateProc.spawn(update__updater(update_st), args)
|
||||
if update_st.update__proc < 0 {
|
||||
update__fail(base_st, update_st, UPDATE_ERR_APPLY)
|
||||
return
|
||||
}
|
||||
update__state = UPDATE_APPLY
|
||||
update__fact(UPDATE_APPLYING)
|
||||
update_st.update__state = UPDATE_APPLY
|
||||
update__fact(base_st, update_st, UPDATE_APPLYING)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue