Skip to content

Commit

Permalink
[sample] Added samples/bootload
Browse files Browse the repository at this point in the history
  • Loading branch information
pajama-coder committed Jul 17, 2024
1 parent e1f6716 commit 23443d2
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
2 changes: 2 additions & 0 deletions samples/bootload/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
script/
script.bak/
23 changes: 23 additions & 0 deletions samples/bootload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Bootloader and Auto-updater

This sample demonstrates a simple script bootloader that is able to check remote codebase updates, download updates and restart script automatically. Downloaded script is kept locally so that the program can still start up successfully after a download failure.

## Usage

To start the bootloader, type:

```sh
pipy bootload/main.js --args http://pipy-repo-address:6060/repo/my-codebase/
```

Or, if `pipy` can be found in the current `$PATH`, you can execute it just by:

```sh
bootload/main.js http://pipy-repo-address:6060/repo/my-codebase/
```

Downloaded script will be saved in the current directory. If a different directory is preferred, add a second argument to the command:

```sh
bootload/main.js http://pipy-repo-address:6060/repo/my-codebase/ /path/to/local/backup/folder
```
150 changes: 150 additions & 0 deletions samples/bootload/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env -S pipy --args

var UPDATE_CHECK_INTERVAL = 5
var CHECK_POINT_INTERVAL = 30
var PIPY_OPTIONS = ['--no-graph']

var url = pipy.argv[1]
var rootDir = os.path.resolve(pipy.argv[2] || '')

if (!url) {
println('missing a codebase URL')
return pipy.exit(-1)
}

if (!url.startsWith('http://')) {
println('invalid codebase URL', url)
return pipy.exit(-1)
}

if (!dirExists(rootDir)) {
println('local directory not found:', rootDir)
return pipy.exit(-1)
}

url = new URL(url)

var client = new http.Agent(url.host)
var currentEtag = undefined
var currentDate = undefined
var currentPID = 0

function backup() {
if (!dirExists(os.path.join(rootDir, 'script.bak'))) {
if (dirExists(os.path.join(rootDir, 'script'))) {
os.rename(
os.path.join(rootDir, 'script'),
os.path.join(rootDir, 'script.bak')
)
}
}
}

function restore() {
if (dirExists(os.path.join(rootDir, 'script.bak'))) {
os.rmdir(os.path.join(rootDir, 'script'), { recursive: true, force: true })
os.rename(
os.path.join(rootDir, 'script.bak'),
os.path.join(rootDir, 'script')
)
}
}

function checkPoint() {
os.rmdir(os.path.join(rootDir, 'script.bak'), { recursive: true, force: true })
}

function fileExists(pathname) {
var s = os.stat(pathname)
return s && s.isFile()
}

function dirExists(pathname) {
var s = os.stat(pathname)
return s && s.isDirectory()
}

function update() {
new Timeout(UPDATE_CHECK_INTERVAL).wait().then(() => {
client.request('GET', url.path).then(res => {
var status = res?.head?.status
if (status !== 200) {
console.error(`Download ${url.href} -> ${status}`)
return update()
}
var headers = res.head.headers
var etag = headers['etag'] || ''
var date = headers['date'] || ''
if (etag === currentEtag && date === currentDate) return update()
console.info(`Codebase changed: etag = '${etag}', date = '${date}'`)
backup()
return Promise.all(
res.body.toString().split('\n').map(
path => client.request('GET', os.path.join(url.path, path)).then(
res => {
var status = res?.head?.status
console.info(`Download ${path} -> ${res.head.status}`)
return (status === 200 ? [path, res.body] : [path, null])
}
)
)
).then(files => {
var base = os.path.join(rootDir, 'script')
var failures = false
files.forEach(([path, data]) => {
if (!data) return (failures = true)
var fullpath = os.path.join(base, path)
var dirname = os.path.dirname(fullpath)
os.mkdir(dirname, { recursive: true })
os.write(fullpath, data)
})
if (failures) {
console.error(`Update failed due to download failure`)
restore()
update()
} else {
console.info(`Update downloaded`)
currentEtag = etag
currentDate = date
start()
new Timeout(CHECK_POINT_INTERVAL).wait().then(() => {
checkPoint()
update()
})
}
})
})
})
}

function start() {
if (currentPID === 0) {
var pathPipy = pipy.argv[0]
var pathMain = os.path.join(rootDir, 'script/main.js')
if (fileExists(pathMain)) {
var command = [pathPipy, ...PIPY_OPTIONS, pathMain]
console.info('Starting', command)
pipeline($=>$
.onStart(new Data)
.exec(command, {
stderr: true,
onStart: pid => {
currentPID = pid
console.info('Started PID =', pid)
},
onExit: () => {
console.info('Process exited PID =', currentPID)
currentPID = 0
},
})
.tee('-')
).spawn()
}
} else {
os.kill(currentPID, 1)
}
}

restore()
start()
update()

0 comments on commit 23443d2

Please sign in to comment.