2022/1/24

Electron Bookmarker

這是 Electron in Action chap2 的 github sample bookmarker github,該章節是第一個 Electron applicaiton sample,裡面使用了 localStorage 及 shell,因為測試的關係,修改使用 IPC,將 shell 呼叫移到 main process。

Electron API 有列出 API 項目,以及該 API 可以使用在 Main/Renderer process 的資訊

package.json

  • 將 electron 版本改為目前的 stable 版
    • "electron": "^13.1.9"
{
  "name": "bookmarker",
  "version": "1.0.0",
  "description": "An example Electron application",
  "main": "app/main.js",
  "scripts": {
    "start": "electron .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/electron-in-action/bookmarker.git"
  },
  "author": "Steve Kinney <hello@stevekinney.net> (http://stevekinney.net)",
  "license": "MIT",
  "private": true,
  "dependencies": {},
  "bugs": {
    "url": "https://github.com/electron-in-action/bookmarker/issues"
  },
  "homepage": "https://github.com/electron-in-action/bookmarker#readme",
  "devDependencies": {
    "electron": "^13.1.9"
  }
}

main.js

  • 把原本放在 renderer process 的 shell 呼叫,移到 main process

  • 增加 IPC main 的部分,處理 renderer process 傳送的訊息及 href 參數,然後呼叫 shell.openExternal(href);

  • 因為新版的 electron 的限制,renderer process 使用了 localStorage,故 BrowserWindow 必須增加兩個 webPreferences 設定

    mainWindow = new BrowserWindow({
          webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
          }
      });
  • 用以下 code 查詢 localStorage 儲存的硬碟路徑

    // 查詢 application 將 localStorage 儲存在哪一個 folder
      let userdata = app.getPath('userData');
      console.log('userdata='+userdata);
const {
  app,
  shell,
  BrowserWindow,
  ipcMain
} = require('electron');

let mainWindow = null; // #A

app.on('ready', () => {
  console.log('Hello from Electron.');

  // 查詢 application 將 localStorage 儲存在哪一個 folder
  let userdata = app.getPath('userData');
  console.log('userdata='+userdata);

  mainWindow = new BrowserWindow({
      webPreferences: {
        nodeIntegration: true,
        contextIsolation: false
      }
  });
  mainWindow.webContents.loadURL(`file://${__dirname}/index.html`); // #A
});

ipcMain.on('shell-openExternal', (event, data) => {
  var href = data.href;
  console.log("shell-openExternal href="+href);
  shell.openExternal(href);
});

index.html

  • 修改 include js 的部分

    <!--     <script>
          require('./renderer');
        </script> -->
        <script src="renderer.js"></script>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="style.css" type="text/css">
    <title>Bookmarker</title>
  </head>
  <body>

    <h1>Bookmarker</h1>

    <div class="error-message"></div>

    <section class="add-new-link">
      <form class="new-link-form">
        <input type="url" class="new-link-url" placeholder="URL" required>
        <input type="submit" class="new-link-submit" value="Submit" disabled>
      </form>
    </section>

    <section class="links"></section>

    <section class="controls">
      <button class="clear-storage">Clear Storage</button>
    </section>

<!--     <script>
      require('./renderer');
    </script> -->
    <script src="renderer.js"></script>

  </body>

</html>

style.css

html {
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body, input {
  font: menu; // #A
}

renderer.js

  • 刪除 shell 呼叫的部分,改用 IPC 傳送到 main process 處理

  • 注意處理 Submit 的寫法

    • 在填寫 newLinkUrl 時,用 validity 檢查是否為正確的 URL 字串,然後將結果,轉為指定 newLinkSubmit enable/disable

    • submit form 的時候,event.preventDefault(); 可避免 Chromium 呼叫原本 form submit 的動作,不會 trigger HTTP request

    • fetch 可取得 URL 的 response,在這邊只需要 title,用來作為 bookmark 的 title。實際上是用了 DOMParser parsing HTTP response data

      newLinkUrl.addEventListener('keyup', () => {
      newLinkSubmit.disabled = !newLinkUrl.validity.valid;
      });
      
      newLinkForm.addEventListener('submit', (event) => {
      event.preventDefault();
      
      const url = newLinkUrl.value;
      
      fetch(url)
        .then(response => response.text())
        .then(parseResponse)
        .then(findTitle)
        .then(title => storeLink(title, url))
        .then(clearForm)
        .then(renderLinks)
        .catch(error => handleError(error, url));
      });
  • localStorage 是 key-value 的形式

    localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
let ipcRenderer = require('electron').ipcRenderer;

// const {shell} = require('electron');

const parser = new DOMParser();

const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');

newLinkUrl.addEventListener('keyup', () => {
  newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});

newLinkForm.addEventListener('submit', (event) => {
  event.preventDefault();

  const url = newLinkUrl.value;

  fetch(url)
    .then(response => response.text())
    .then(parseResponse)
    .then(findTitle)
    .then(title => storeLink(title, url))
    .then(clearForm)
    .then(renderLinks)
    .catch(error => handleError(error, url));
});

clearStorageButton.addEventListener('click', () => {
  localStorage.clear();
  linksSection.innerHTML = '';
});

linksSection.addEventListener('click', (event) => {
  if (event.target.href) {
    event.preventDefault();
    var data = {
      href: event.target.href
    };
    // shell.openExternal(event.target.href);
    ipcRenderer.send('shell-openExternal', data);

  }
});


const clearForm = () => {
  newLinkUrl.value = null;
}

const parseResponse = (text) => {
  return parser.parseFromString(text, 'text/html');
}

const findTitle = (nodes) => {
  return nodes.querySelector('title').innerText;
}

const storeLink = (title, url) => {
  localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
}

const getLinks = () => {
  return Object.keys(localStorage)
               .map(key => JSON.parse(localStorage.getItem(key)));
}

const convertToElement = (link) => {
  return `<div class="link"><h3>${link.title}</h3>
          <p><a href="${link.url}">${link.url}</a></p></div>`;
}

const renderLinks = () => {
  const linkElements = getLinks().map(convertToElement).join('');
  linksSection.innerHTML = linkElements;
}

const handleError = (error, url) => {
  errorMessage.innerHTML = `
    There was an issue adding "${url}": ${error.message}
  `.trim();
  setTimeout(() => errorMessage.innerText = null, 5000);
}

const validateResponse = (response) => {
  if (response.ok) { return response; }
  throw new Error(`Status code of ${response.status} ${response.statusText}`);
}

renderLinks();

執行畫面

References

Electron in Action

Electron in Action repositories

Electron in Action source code

require is not defined

Where an electron application's sessionStorage and localStorage stored?

electron-localStorage,如何在主进程和渲染进程中使用?

2022/1/17

Electron Clock

Electron Clock 跟 Alarm 的 tutorial

建立專案

mkdir electron-alarm-clock && cd electron-alarm-clock
npm init -y

安裝 electron,會安裝到 electron-alarm-clock/node_modules 裡面

npm install --save--dev electron

修改 package.json,"main" 及 "scripts" 的部分

{
  "name": "electron-alarm-clock",
  "version": "1.0.0",
  "description": "",
  "main": "app/main.js",
  "scripts": {
    "start": "electron .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "electron": "^13.1.9"
  }
}

建立 main.js

const {
    app,
    BrowserWindow
} = require('electron')

const path = require('path')
const url = require('url')

app.on('ready', createWindow)

app.on('window-all-closed', () => {
    // darwin = MacOS
    if (process.platform !== 'darwin') {
       return false;
    }
    app.quit()
})

app.on('activate', () => {
    if (win === null) {
        createWindow()
    }
})

function createWindow() {
    // Create the browser window.
    win = new BrowserWindow({
        width: 400,
        height: 400,
        maximizable: false,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
        }
    })

    win.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }))

    // Open DevTools.
    // win.webContents.openDevTools()

    // When Window Close.
    win.on('closed', () => {
        win = null
    })

    // When Window Minimize
    win.on('minimize', () => {
        win.hide()
    })

}

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Electron</title>
</head>

<body>
    <h1>Hello Electron</h1>
    <p>Node Version:
        <script>document.write(process.versions.node)</script>
    </p>
    <p>Chrome Version:
        <script>document.write(process.versions.chrome)</script>
    </p>
    <p>Electron Version:
        <script>document.write(process.versions.electron)</script>
    </p>
</body>

</html>

啟動

npm start


Clock

安裝 moment 套件

npm install --save moment

修改 index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Electron</title>
</head>

<body>
    <h1>Hello Electron</h1>
    <p>Node Version:
        <script>document.write(process.versions.node)</script>
    </p>
    <p>Chrome Version:
        <script>document.write(process.versions.chrome)</script>
    </p>
    <p>Electron Version:
        <script>document.write(process.versions.electron)</script>
    </p>
    <hr>
    <div class="now-time"></div>
    <input type="text" class="alarm-time">

    <script src="app.js"></script>
</body>

</html>

新增 app.js

const moment = require('moment')

const elNow = document.querySelector('.now-time')
const elAlarm = document.querySelector('.alarm-time')
elAlarm.addEventListener('change', onAlarmTextChange)

let time = moment()

// 目前的時間
let nowTime
// alarm 時間,預設為目前的時間 + 5 seconds
let alarmTime

/** Set Time */
const now = moment(time).format('HH:mm:ss')
nowTime = now
elNow.innerText = now

const alarm = moment(time).add(5, 'seconds').format('HH:mm:ss')
alarmTime = alarm
elAlarm.value = alarm

timer()

/** Now Time */
function timer() {
    time = moment().format('HH:mm:ss')

    /** Set Now */
    nowTime = time
    elNow.innerText = time

    setTimeout(() => {
        timer()
    }, 1000)
}

/**
 * Save To Global Variable,
 * Can't Read Dom In Minimize Status.
 * @param {event} event
 */
function onAlarmTextChange(event) {
    alarmTime = event.target.value
}

啟動後,會看到畫面上增加 clock 部分,時間會一直不斷地更新

Alarm

安裝 node-notifier 套件

npm install --save node-notifier

修改 app.js

const notifier = require('node-notifier')
const path = require('path')

const moment = require('moment')

const elNow = document.querySelector('.now-time')
const elAlarm = document.querySelector('.alarm-time')
elAlarm.addEventListener('change', onAlarmTextChange)

let time = moment()

// 目前的時間
let nowTime
// alarm 時間,預設為目前的時間 + 5 seconds
let alarmTime

/** Set Time */
const now = moment(time).format('HH:mm:ss')
nowTime = now
elNow.innerText = now

const alarm = moment(time).add(5, 'seconds').format('HH:mm:ss')
alarmTime = alarm
elAlarm.value = alarm

timer()

/** Now Time */
function timer() {
    time = moment().format('HH:mm:ss')

    /** Set Now */
    nowTime = time
    elNow.innerText = time

    check()

    setTimeout(() => {
        timer()
    }, 1000)
}

/** Check Time */
function check() {
    const diff = moment(nowTime, 'HH:mm:ss').diff(moment(alarmTime, 'HH:mm:ss'))
    if (diff === 0) {
        //alert('wake up!')
        const msg = "It's" + alarmTime + ". Wake Up!"
        /** const msg = `It's ${alarmTime}. Wake Up!` */
        notice(msg)
    }
}

/**
 * System Notification
 * @param {string} msg
 */
function notice(msg) {

    /** https://github.com/mikaelbr/node-notifier */
    notifier.notify({
        title: 'Alarm Clock',
        message: msg,
        icon: path.join(__dirname, 'clock.ico'),
        sound: true,
    })
}

/**
 * Save To Global Variable,
 * Can't Read Dom In Minimize Status.
 * @param {event} event
 */
function onAlarmTextChange(event) {
    alarmTime = event.target.value
}

Tray

讓 application 能夠隱藏到系統列

修改 main.js

const {
    app,
    BrowserWindow,
    Tray,
    Menu,
} = require('electron')
function createWindow() {
    // ........ skipped

    // When Window Close.
    win.on('closed', () => {
        win = null
    })

    // Create Tray
    createTray()

}

// 這邊是 windows 的寫法, mac 沒有出現這兩個選單
function createTray() {
    let appIcon = null
    const iconPath = path.join(__dirname, 'clock.ico')

    const contextMenu = Menu.buildFromTemplate([{
            label: 'AlarmClock',
            click() {
                win.show()
            }
        },
        {
            label: 'Quit',
            click() {
                win.removeAllListeners('close')
                win.close()
            }
        }
    ]);

    appIcon = new Tray(iconPath)
    appIcon.setToolTip('Alarm Clock')
    appIcon.setContextMenu(contextMenu)

}

IPC

透過 IPC,能夠從 renderer process 呼叫 main process,重新將 application 顯示在前景

修改 app.js

let ipcRenderer = require('electron').ipcRenderer;


/**
 * System Notification
 * @param {string} msg
 */
function notice(msg) {

    /** https://github.com/mikaelbr/node-notifier */
    notifier.notify({
        title: 'Alarm Clock',
        message: msg,
        icon: path.join(__dirname, 'clock.ico'),
        sound: true,
    })

    /** Show Application */
    ipcRenderer.send('show-main-window');
}

利用 ipcRenderer.send('show-main-window'); 發送 "show-main-window" 給 main process

修改 main.js

const {
    app,
    BrowserWindow,
    Tray,
    Menu,
    ipcMain
} = require('electron')

//> ipcMain is ipc of main process
//> ipcMain listen to show-main-window channel here
ipcMain.on('show-main-window', () => {
    console.log('show-main-window by ipc');
    win.show();
});

當程式啟動後,把 application 縮小到背景,alarm 時間到了的時候,就會透過 IPC 將 application window 顯示到前景,同時產生一個系統的 notification

References

Electron - 新手入門 - 做一個鬧鐘吧

[Electron] IPC 機制

2022/1/10

nvm

nvm 是 Node.js 的版本管理器,可在同一台主機上安裝多個版本的 Node.js 環境,因為不同專案可能會使用不同的 NodeJS 版本,nvm 可讓不同版本的 NodeJS 並存,且可以動態因應專案而切換。

安裝

nvm 是一個 shell script,不是執行檔,安裝也直接用 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash 安裝即可

~$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 14926  100 14926    0     0  37258      0 --:--:-- --:--:-- --:--:-- 38370
=> Downloading nvm from git to '/Users/charley/.nvm'
=> 正複製到 '/Users/charley/.nvm'...
remote: Enumerating objects: 347, done.
remote: Counting objects: 100% (347/347), done.
remote: Compressing objects: 100% (295/295), done.
remote: Total 347 (delta 39), reused 161 (delta 27), pack-reused 0
接收物件中: 100% (347/347), 196.36 KiB | 486.00 KiB/s, 完成.
處理 delta 中: 100% (39/39), 完成.
* (開頭指標分離於 FETCH_HEAD)
  master
=> Compressing and cleaning up git repository

=> Appending nvm source string to /Users/charley/.bash_profile
=> Appending bash_completion source string to /Users/charley/.bash_profile
=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

nvm 會安裝到 /Users/user/.nvm 使用者的 ~/.nvm 目錄中,同時將以下的環境變數增加到 profile 設定 ( ~/.bash_profile~/.zshrc~/.profile~/.bashrc )

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

安裝後,可用以下指令確認安裝完成

command -v nvm

常用指令

版本號碼

$ nvm --version
0.38.0

ls

list 目前已經安裝的 nodejs

$ nvm ls
->     v14.17.5
default -> v14.17.5
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v14.17.5) (default)
stable -> 14.17 (-> v14.17.5) (default)
lts/* -> lts/fermium (-> v14.17.5)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.5 (-> N/A)
lts/fermium -> v14.17.5

list 遠端可安裝的 NodeJS 版本

nvm ls-remote

list 遠端可安裝的 NodeJS 版本,且限制 LTS 版本

nvm ls-remote --lts

install

安裝 NodeJS

nvm install v14.17.5

use

切換版本

nvm use v14.17.5

current

查詢目前版本

nvm current

run, exec

執行 NodeJS

nvm run node

執行特定版本的 node

nvm exec 12.8.1 node

which

查閱 nodejs 安裝路徑

$ nvm which v14.17.5
/Users/charley/.nvm/versions/node/v14.17.5/bin/node

alias

$ nvm alias
default -> v14.17.5
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v14.17.5) (default)
stable -> 14.17 (-> v14.17.5) (default)
lts/* -> lts/fermium (-> v14.17.5)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.5 (-> N/A)
lts/fermium -> v14.17.5

可以設定別名

nvm alias erbium v14.17.5

修改 default 版本

nvm alias default v14.17.5

npm

config

$ npm config ls
; cli configs
metrics-registry = "https://registry.npmjs.org/"
scope = ""
user-agent = "npm/6.14.14 node/v14.17.5 darwin x64"

; project config /Users/charley/.nvm/.npmrc
package-lock = false

; node bin location = /Users/charley/.nvm/versions/node/v14.17.5/bin/node
; cwd = /Users/charley/.nvm
; HOME = /Users/charley
; "npm config ls -l" to show all defaults.

list 所有設定

npm config ls -l

可修改 prefix, cache 路徑

npm config set prefix "/Users/charley/.nvm/versions/node/v14.17.5"
npm config set cache "/Users/charley/.npm"

安裝 npm 套件

npm install requirejs -g
npm install uglify-js -g
npm install less -g

套件會安裝到這個目錄

~/.nvm/versions/node/v14.17.5/lib/node_module

切換 NodeJS 版本時, npm library 也要重新安裝

.nvmrc

可在專案目錄中,建立一個 .nvmrc 檔案,裡面記錄 NodeJS 版本號碼

echo "14.17.5" > .nvmrc

echo "lts/*" > .nvmrc # to default to the latest LTS version

echo "node" > .nvmrc # to default to the latest version

切換到專案後,可以用 use 指令,直接切換專案的 NodeJS 版本

nvm use

nvm github 有記錄如何切換目錄後,自動切換 node 版本的 script,但目前覺得不需要那麼自動

References

Mac 安裝 NVM

macOS安裝與移除NVM

Node.js 環境設定-for mac

nvm:安裝、切換不同 Node.js 版本的管理器

How to install Node.js and npm on macOS

NVM、NPM、Node.js的安裝選擇

2022/1/3

Erlang Appup Cookbook

.appup applicaiton upgrade files 可支援系統動態更新,裡面描述如何 upgrade/downgrade a running system,這個檔案會被 systools 的 relup 使用,產生 release upgrade file

application.appup

applicaiton.appup 必須要出現在 application 的 ebin 目錄

該檔案內容只包含一個 erlang term,定義 upgrade/downgrade application 的動作

{Vsn,
  [{UpFromVsn, Instructions}, ...],
  [{DownToVsn, Instructions}, ...]}.
% 目前 application version
Vsn = string()

% 更新自哪一版 application version
UpFromVsn = string() | binary()

% 降版時,要降成哪一個 application version
% 可使用 binary regex
% 例如 <<"2\\.1\\.[0-9]+">> 就是 2.1.*
DownToVsn = string() | binary()

% release upgrade instructions
Instructions

Release Upgrade Instructions

分為 high-level/low-level instructions,建議使用 high-level,systool 會自動產生 low-level instructions

high-level instructions

{update, Mod}
{update, Mod, supervisor}
{update, Mod, Change}
{update, Mod, DepMods}
{update, Mod, Change, DepMods}
{update, Mod, Change, PrePurge, PostPurge, DepMods}
{update, Mod, Timeout, Change, PrePurge, PostPurge, DepMods}
{update, Mod, ModType, Timeout, Change, PrePurge, PostPurge, DepMods}
  Mod = atom()
  ModType = static | dynamic
  Timeout = int()>0 | default | infinity
  Change = soft | {advanced,Extra}
    Extra = term()
  PrePurge = PostPurge = soft_purge | brutal_purge
  DepMods = [Mod]

所有 processes 都會被 sys:suspend 暫停,載入新版 module 後,以 sys:resume 繼續執行

  • Change

    預設為 soft,定義 type of code change

    設定為 {advanced, Extra} 時,以 gen_server, gen_fsm, gen_statem, gen_event 時做的 processes,會呼叫 code_change 改變內部狀態, Extra 為參數

  • PrePurge

    預設為 brutal_purge

    載入新版 code 以前,控制執行舊版 code 的 processes 的動作

    brutal_purge 就是把 process killed

    soft_purgerelease_handler:install_release/1 returns {error,{old_processes,Mod}}.

  • PostPurge

    預設為 brutal_purge

    載入新版 code 時,控制執行舊版 code 的 processes 的動作

    brutal_purge,當 release 確認永久生效時,code 被 purged,process 被 killed

    soft_purge,在沒有 process 執行舊版的 code 時,release handler 會 purges old code

  • DepMods

    預設為 []

    定義 Mod 跟哪些 modules 相依

    在 upgrading/downgrading 時,DepMods 的 processes,會被 suspended

  • Timeout

    定義 suspending processes 的 timeout 時間

  • ModType

    預設為 dynamic

    dynamic 表示 process 會自動使用新版的 code

    static,process 被詢問是否要 change code 以前,就會載入新版 code

supervisor 的 update,是用來修改 start spec


  • 更新 module Mod
{load_module, Mod}
{load_module, Mod, DepMods}
{load_module, Mod, PrePurge, PostPurge, DepMods}
  Mod = atom()
  PrePurge = PostPurge = soft_purge | brutal_purge
  DepMods = [Mod]
  • 載入新的 module Mod
{add_module, Mod}
{add_module, Mod, DepMods}
  Mod = atom()
  DepMods = [Mod]
  • 刪除 module
{delete_module, Mod}
{delete_module, Mod, DepMods}
  Mod = atom()
  • 新增 application
{add_application, Application}
{add_application, Application, Type}
  Application = atom()
  Type = permanent | transient | temporary | load | none
  • 刪除 application
{remove_application, Application}
  Application = atom()
  • restart application
{restart_application, Application}
  Application = atom()

example

{“0.2.0”,
  [{“0.1.0”, [
              {add_module, state_handler}
             ,{update, nine9s_sup, supervisor}
             ,{apply, {supervisor, restart_child, [nine9s_sup, state_handler]}}
             ,{load_module, default_handler}
             ,{add_module, count_handler}
             ,{load_module, nine9s_app}
             ,{apply, {nine9s_app, set_routes_new, [] }} ] }],
  [{“0.1.0”, [
              {load_module, default_handler}
             ,{apply, {supervisor, terminate_child, [nine9s_sup, state_handler]}}
             ,{apply, {supervisor, delete_child, [nine9s_sup, state_handler]}}
             ,{update, nine9s_sup, supervisor}
             ,{delete_module, state_handler}
             ,{apply, {nine9s_app, set_routes_old, [] }}
             ,{delete_module, count_handler}
             ,{load_module, nine9s_app}
             ]
}]}.

upgrade

  • {add_module, state_handler}
    • 增加 state_handler module
  • {update, nine9s, supervisor}
    • 修改 nine9s supervisor 內部狀態 及 spec
  • {apply, {supervisor, restart_child, [nine9s, state_handler]}}
    • apply 會執行 M:F(A1, … An)
    • 也就是執行 supervisor:restart_child(nine9s, state_handler)
  • {load_module, default_handler}
    • 載入 default_handler module,取代舊版的 code
  • {add_module, count_handler}
    • 增加 count_handler module
  • {load_module, nine9s_app}
    • 載入 nine9s_app module
  • {apply, {nine9s_app, set_routes_new, [ ] }} ] } ]
    • 執行 nine9s_app:set_routes_new()

downgrade

  • {load_module, default_handler}
    • 載入舊版的 default_handler
  • {apply, {supervisor, terminate_child, [nine9s_sup, state_handler]}}
    • 停止 state_handler process
  • {apply, {supervisor, delete_child, [nine9s_sup, state_handler]}}
    • nine9s_sup 裡面刪除 state_handler
  • {update, nine9s_sup, supervisor}
    • 修改 nine9s_sup 的內部狀態
  • {delete_module, state_handler}
    • 删除 state_handler module
  • {apply, {nine9s_app, set_routes_old, [ ] }}
    • 設定舊版的 routes
  • {delete_module, count_handler}
    • 刪除 count_handler module
  • {load_module, nine9s_app}
    • 載入舊版的 nine9s_app module

example of appup

  • 更新 functional module

一般功能性的 module 修改後,都可以直接更新

{"2",
 [{"1", [{load_module, m}]}],
 [{"1", [{load_module, m}]}]
}.
  • 修改常駐 module

除了 system processes, special processes 以外,常駐 module 就是supervisor, gens_server, gen_fsm, gen_statem, gen_event 其中之一。

  • 修改 callback module

跟一般功能性的 module 修改一樣

{"2",
 [{"1", [{load_module, ch3}]}],
 [{"1", [{load_module, ch3}]}]
}.
  • 修改 internal state

process 需要呼叫 code_change

{"2",
 [{"1", [{update, ch3, {advanced, []}}]}],
 [{"1", [{update, ch3, {advanced, []}}]}]
}.
-module(ch3).
...
-export([code_change/3]).
...
% downgrade 時呼叫
code_change({down, _Vsn}, {Chs, N}, _Extra) ->
    {ok, Chs};
% upgrade 時呼叫
code_change(_Vsn, Chs, _Extra) ->
    {ok, {Chs, 0}}.
  • module dependencies
{load_module, Module, DepMods}
{update, Module, {advanced, Extra}, DepMods}

m1 跟 ch3 相依

myapp.appup:

{"2",
 [{"1", [{load_module, m1, [ch3]}]}],
 [{"1", [{load_module, m1, [ch3]}]}]
}.

ch_app.appup:

{"2",
 [{"1", [{load_module, ch3}]}],
 [{"1", [{load_module, ch3}]}]
}.
{"2",
 [{"1",
   [{load_module, ch3},
    {load_module, m1, [ch3]}]}],
 [{"1",
   [{load_module, ch3},
    {load_module, m1, [ch3]}]}]
}.
  • 修改 special process 的 code

ch4 in proc_lib 由 supervisor 啟動, child spec 為

{ch4, {ch4, start_link, []},
 permanent, brutal_kill, worker, [ch4]}

ch4 是 sp_app 的其中一個 application,在更新 1-> 2 時,必須要載入新版的 module

{"2",
 [{"1", [{update, ch4, {advanced, []}}]}],
 [{"1", [{update, ch4, {advanced, []}}]}]
}.
-module(ch4).
...
-export([system_code_change/4]).
...

system_code_change(Chs, _Module, _OldVsn, _Extra) ->
    {ok, Chs}.
  • 更新 supervisor

可修改 restart strategy 與 maximum restart frequency property

{update, Module, supervisor}
{"2",
 [{"1", [{update, ch_sup, supervisor}]}],
 [{"1", [{update, ch_sup, supervisor}]}]
}.

修改 child spec

{"2",
 [{"1", [{update, ch_sup, supervisor}]}],
 [{"1", [{update, ch_sup, supervisor}]}]
}.

add/delete child processes

{"2",
 [{"1",
   [{update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
   ]}],
 [{"1",
   [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor}
   ]}]
}.
{"2",
 [{"1",
   [{add_module, m1},
    {update, ch_sup, supervisor},
    {apply, {supervisor, restart_child, [ch_sup, m1]}}
   ]}],
 [{"1",
   [{apply, {supervisor, terminate_child, [ch_sup, m1]}},
    {apply, {supervisor, delete_child, [ch_sup, m1]}},
    {update, ch_sup, supervisor},
    {delete_module, m1}
   ]}]
}.
  • add/delete module
{"2",
 [{"1", [{add_module, m}]}],
 [{"1", [{delete_module, m}]}]
  • start/terminate a process

  • add/remove application

  • restart application

{"2",
 [{"1", [{restart_application, ch_app}]}],
 [{"1", [{restart_application, ch_app}]}]
}.
  • 修改 application spec
{"2",
 [{"1", []}],
 [{"1", []}]
}.
  • 修改 application configuration

也就是更新 sys.config

  • 修改 included applications

  • 修改 non-erlang code

  • emulator restart and upgrade

{"B",
 [{"A",
   [],
   [restart_emulator]}],
 [{"A",
   [],
   [restart_emulator]}]
}.

References

Appup

Appup Cookbook

用Rebar3热更新Erlang代码

Appup Cookbook cn