DAXクエリでPower AutomateからPower BIデータセットを読む

背景

Power BIで色々見える化する際に、単にレポートを公開するだけでなく、定期レポートのような形でメールなりチャットなりで現在の状況を端的に通知したいというニーズがあった。
Power BIレポートからデータを取得しようと思うと、以下のような方法があるが、前者はテキストデータを取得できない(図表を画像で取得することはできる)、後者はデータ取得のために色々不要なオブジェクトを作る必要があるなど、使い勝手がいまいちだった。

レポートではなくその源流にあるデータセットに対してクエリを実行することができることを知ったので、やり方を調べてみた。

最終的に実現したいこと

Power Automateでデータ更新間隔に応じて日次・月次などのスケジュールで動くフローを作成し、定期レポートを配信する。

DAXクエリを試す環境の準備

Power Automate上でクエリを書いてトライアンドエラーを繰り返すのは何とも効率が悪い。特に慣れない言語を使うときには高速でトライアンドエラーを回せる環境を整えるのがとても大事。今回はMSの公式ドキュメントで言及されていたDAX Studioを使ってみる。
ローカルのPower BI Desktopでpbixファイルを開いていれば、DAX Studioを使ってpbixファイルの中のデータセットに接続してクエリを試せるらしい。
というわけで以下のようにしてクエリを試す環境を整えた。

  1. 公式サイトからDAX Studioのインストーラーを入手してインストール
  2. Power BIサービスからクエリを発行したいデータセットをダウンロード
  3. ダウンロードしたpbixファイルをPower BI Desktopで開く
  4. DAX Studioを開いてデータセットに接続

DAXクエリを書く

DAXクエリの基本

DAX クエリ - DAX | Microsoft Learnに書いてあるが、要点は以下の通り。

  • DAXクエリの返り値はテーブル
  • EVALUATE <テーブルを返すDAX式>が最も基本的な構文
  • DEFINEステートメントを使って変数を定義することができる

クエリについて学習する前に、DAX の基礎を十分に理解しておくことが重要です。 まだ十分に理解していない場合は、必ず「DAX の概要」を確認してください。

とのこと。いつも雰囲気でDAX式を使っているので、この機会に勉強しなおすことにする。

DAX式の基本

DAX関数

Excel関数によく似たDAX関数を組み合わせて目的の値を得るための式を構築することが基本になる。

データ型

DAX関数の引数や返り値で使うデータの型。公式ドキュメントがうまくまとまっていないので整理してみる。
一般に型と言えば、数値や文字列などの最小のデータ単位を表現するための基本型と、複数の基本型のデータを組み合わせて構造を持たせた複合型(言語によって呼び方が違う)があるが、DAX関数のドキュメントは以下のような状況。

  • 基本データ型は見つけやすいところに記載がある
  • 複合データ型が分かりやすく説明されたページが見つからないので自力で理解するしかない…
DAX式で使える複合データ型

色々見ながら私が理解した内容を記載しています。ということなので間違っている可能性があります。

テーブル

型(基本型)を持った列によって定義された行の集まりによって構成される表構造。要は普通のRDBのテーブルをイメージすればよい。
DAXクエリでは返り値をテーブル型にする必要があるので、EVALUATEステートメントの直下に記述する関数はテーブル型を返すことになる。

式の中ではシングルクオート'で囲み、'テーブル名'のようにして参照できる。 式の中でリテラルでテーブルを書くこともでき、ここに説明があって、DAXクエリでは以下のように試せる。

DEFINE TABLE '複数列から成るテーブル' = {("row1", 1), ("row2", 2), ("row3", 3)}
EVALUATE '複数列から成るテーブル'
Value1 Value2
row1 1
row2 2
row3 3
リスト(列が1つのテーブル)

どうやら厳密に言うとリスト型とういうものがDAXにはなく、列が1つのテーブルを使う様子。書き方は上と同じページに説明がある

DEFINE TABLE '1列から成るテーブル' = {1,2,3}
EVALUATE '1列から成るテーブル' 
Value
1
2
3

例えばIN演算子を書くときに使う。
ネストされたリストも作れるらしく、例えばDATATABLE関数の引数になるが、それ以外の使い道は不明。

Tableの中の単一の列に対する参照であり、実際に処理される際には行コンテキスト(後述)によって特定される基本型の具体的な値として評価される。
'Table'[Column]のようにして[ ]で囲んで指定する。
関数の引数として列そのものを指定したり、引数として指定するDAX式の中で使う。

コンテキスト

DAXを理解するキモになるのがおそらくコンテキスト。
DAX関数に渡す引数は"Value"のようにリテラルで定数を記述する場合を除き、テーブルや列になる。
[Column1] + [Column2]あるいはSUM('Table'[Column])のような式を書いたときに、「計算に含まれるのがどの行の値なのか」を決めるのがコンテキスト。
列名による参照のスコープと言い換えても良いかもしれない。
例えば以下のような場合にコンテキストが重要になる。

  • 累計値を得るために<該当行の日時以前の日時を持つすべての行の値の合計>を計算する
  • 販売シェア(%)を得るために<自社製品の販売個数の合計値> / <全製品の販売個数の合計値>を計算する
行コンテキスト

正確な解説はここが詳しい。
テーブルの行を一行ずつ扱う処理において、列名による参照を「現在処理されている行」に束縛するのが行コンテキスト。
行コンテキストが存在する場合、[Column1] + [Column2]の計算に使われる値は、現在処理されている行のColumn1とColumn2の値となる。

では「行コンテキストが存在する場合」とはどのような場合かというと、以下の2パターンがある。

  1. あるテーブルに作られた「計算列」(例:[Column1] + [Column2])のDAX式が処理されるときには、テーブルの各行が順番に処理されていき、DAX式の中で指定された列の参照先は行コンテキストによって束縛される(つまり、現在処理されている行の該当列の値が参照される)
  2. テーブルを引数として受け取るDAX関数(SUMX、FILTERなど)は与えられたテーブルの全行を順番に処理していく。多くは「処理の内容」を2つ目以降の引数で受け付けるようになっている(例えばFILTERはフィルタ条件のDAX式を第二引数で受け取る)が、「処理の内容」の中で記載された列名の参照先は行コンテキストによって束縛される(つまり第1引数で与えられたテーブルの現在処理されている行の該当列の値が参照される)
行コンテキストのネスト

理解が難しかったのが行コンテキストのネスト。ある行を処理しているときに「複数の行を含むテーブル」を参照しながら値を作る必要がある場合に行コンテキストのネストが発生する。
もう少し具体的に言うと、上記の2のパターン(テーブルを引数として受け取るDAX関数)が行コンテキストの中で現れた時、現在の行コンテキストの中で新しく「テーブルの全行を順番に処理する」ことになり、すなわち新しい行コンテキストが入れ子状に発生する。

例えば、累計値を計算したい場合には「現在の行の日時以前の日時を持つすべての行」を含むテーブルを参照する必要がある。
そのような処理を例えばforループで記述するときにはループがネストするはずである。

for (let row_context of ) {
  const current_datetime = row_context.datetime;
  let cumulative_val = 0;
  for (let nested_row_context of table) {
    if (nested_row_context.datetime <= current_datetime) {
      cumulative_val += nested_row_context.val;
    }
  }
}

このような処理を想定したとき、最初のループの中で1つの行コンテキストを処理している最中に、新しいループができて新しいループの中の複数の行コンテキストが処理されるという流れになる。
DAXにはネストされた行コンテキストの中から外側の行コンテキストの値を参照する仕組み(上記の実装例の中のcurrent_datetimeに相当)が用意されている。EARLIER関数がそれで、ここここが詳しい。

EARLIER関数の典型的なユースケースが累計の計算であり、上記の処理と同じことをDAX関数では以下のように記述できる。

SUMX(FILTER('Table', 'Table'[datetime] <= EARLIER('Table'[datetime])), 'Table'[val])
フィルターコンテキスト

公式ドキュメントではDAXのコンテキストは行コンテキスト・クエリコンテキスト・フィルターコンテキストの3つだと書いてあるが、どうやらクエリコンテキストはフィルターコンテキストの亜種らしいのと、Power BIレポートでは重要だがDAXクエリには関係なさそうなので省略。
例によってここが詳しい。

DAX式の中でDAX関数の引数などでテーブル名を指定したとき、テーブル名による参照を「あるフィルタ条件を満たす行」のみに束縛するのがフィルターコンテキスト。
要はあるDAX式が評価されるときにはテーブルにどのようなフィルタがかかっているか というのがフィルターコンテキスト。
フィルタは列に対する条件(例:[Column1] >= 30)であり、リレーションシップが定義されたテーブル間で伝播する。

DAXではフィルターコンテキストを明示的に操作する関数が用意されており、CALCULATE, CALCULATETABLEフィルター関数がそれにあたる。
例えば自社製品の販売シェアを得るために以下のように書ける。

SUMX(FILTER('Sales', [Company] = "My Company"), [Sales Amount]) / SUMX('Sales', [Sales Amount])

DAXクエリの作成(まだ書いてない)

DAXクエリのPower Automateへの組み込み(まだ書いてない)

まとめ(まだ書いてない)

家計簿アプリの情報をBIツールに入れて家計のダッシュボードを作ってみた

概要

クレジットカードとか銀行とかの情報が自動で取り込まれるのが便利な某家計簿アプリを利用しており、アプリ上でもちょっとした分析ができるようにはなっていたものの、アプリの機能に無いグラフも含め、欲しいグラフが1画面で見れるようなダッシュボードが欲しくなった。
そこで、以下のような感じで情報を取ってきて整えてダッシュボードに表示するような仕組みを作ってみた。
(個別の記事を追加していく予定。すでに書いたものはリンクあり)

  • 家計簿アプリからの情報取得
    Puppeteerで家計簿アプリにアクセスして家計簿アプリのエクスポート機能を叩くようなスクリプトを作成してRundeckで定期的に実行するようにした。

  • 取得した情報の整形
    家計簿アプリ上の情報は、同じ店で払った金額についてのレコードでも支払い方法(クレジットカード、非接触型決済、etc.)によって店名の表記にばらつきがあり、分析に利用できない状態だったため、
    「表記が一致しないが同一の意味をもつ摘要を集約」し、「摘要や口座名等によって利用店舗や受益者等の分析用情報を付加」するツールを作って情報を整えるようにした。

    • 某家計簿アプリからエクスポートしたデータを分析用に整えるツールを作った
  • 家計簿情報をダッシュボードに表示
    これまでBIツールはPowerBI Desktopを使っていたが、個人で気軽に使うにはいちいち学習コストが高く、デスクトップアプリなので分析結果を家族に共有するにも不便だったため、Webで動く使いやすいBIツールを探したところ、Metabaseがちょうどいい感じだったので採用。
    整形後のデータをMetabaseで読み込み、欲しい情報だけが表示されるダッシュボードを作った。

    • Metabaseのダッシュボードを別のアプリに組み込むところで若干手入れが必要だったのでネタはあるが記事にするかは微妙…

構成

TODO: システム構成図を描く

作ったグラフの例

たまに便利だけどほとんど趣味の世界なのは放っておいてほしい。

最近のクレジットカードは〇〇で利用すると還元率○%!それ以外は△%ね!みたいなのばっかなので実際の還元率がみたかった。

f:id:miyataro32:20210117003147p:plain
クレジットカードの利用状況と実際の還元率

投信の積み立て用のプール金が減ってきたときに検知できるように。

f:id:miyataro32:20210117004159p:plain
ジュニアNISA購入用に子供の証券口座に入れてある預け金の残高

この分析によって特定のスーパーの利用を避ければ食費が少し減ることが分かってきた

f:id:miyataro32:20210117002744p:plain
特定期間の店ごとの支出を分析するためのバブルチャート

パスワードロックされたExcel VBAのソースコードを見る

背景

はるか昔、人々はExcel VBAを使って今では考えられないほど大規模なツールを構築していました。
現代のとある企業で、そのようなレガシーツールを現代的なWebシステムとして再構築することになり、まずは現行ツールが何をしているか紐解く必要がありました。
そのxlsファイルは今でもたくさんの人が使っているものでしたが、VBAプロジェクトが保護されており、ソースコードを見るためにはパスワードが必要でした。
しかし、パスワードの伝承はすでに途絶えて久しく、保護を解除することができる人はいませんでした。
そこで人々はパスワードを破るすべを探し求め、ついにはソースコードを見ることに成功しました。
※この記事はパスワードを破ることを推奨するものではありません。この記事に記載されている方法はそれ以外に方法がない場合にExcelツールの権利保有者の許可を得た上で試してください。

調査

とりあえずググってみたところ、VBAプロジェクトのロックを一時的に解除するVBAスクリプトが広く知られているようでした。

puu-0328.hatenablog.com

しかし、試してみたところロック解除できた旨のメッセージが表示されるのにVBAプロジェクトを選択したらExcelが落ちる。。
恐らくこの方法はExcel2007以降のファイル(xlsx)用なのだろうと推測し、他の方法を探しました。

すると出てきたのがこれ。 stackoverflow.com

なんか一番人気の回答にxlsでもスクリプトで解除できる的なことが書いてあるけど、一度試してできなかったので無視。
この回答を参考にしました。

Is there a way to crack the password on an Excel VBA Project? - Stack Overflow

結局どうすればいいの?

A: バイナリエディタでパスワードを書き換えます。

詳しくは上記のサイトを見れば分かりますが、ざっと以下のような手順です。

  1. 新しくxls形式のExcelファイルを作成し、VBAをパスワード保護する
  2. バイナリエディタで1で作成したファイルを開き、CMG=... DPB=... GC=...と書いてある箇所を探してコピー
  3. バイナリエディタVBAのパスワードが分からないExcelファイルを開き、2でコピーした値で対応する箇所を上書き

1Passwordに保管した認証情報を使ってPuppeteerでスクレイピングする

概要

ユーザーID、パスワード、ワンタイムパスワードによる認証が必要なWebサイトの情報をスクレイピングで取得したくなったので、比較的安全そうなやり方として1Passwordから情報を引っ張りながら認証を突破する方法を考えてみました。
WLS2上で動くUbuntu20.04.1で実行しています。
利用するツールは以下の通り。

  • node.js
  • Puppeteer

基本となる上記2つに加えて、node.jsの中からchild_process.execSyncを叩いて以下のOSコマンドを利用します。

4つともググれば相当数の解説記事がヒットするので、個別の説明は控えます。

実装

準備

1Password CLIを入手してパスの通ったところにopという実行ファイルを配置した後、サインインしておきます。

op signin <1Passwordの認証ドメイン> <メールアドレス> <SECRET KEY> 

node.jsの中からopコマンドを呼び出して1Passwordのロックを解除する

まずは1Passwordのロックを解除し、1Passwordのセッショントークンを取得します。
execSyncじゃなくてspawnを使えばexpectに頼らずに対話式の認証を突破できそうな雰囲気があったけど、少し理解に手間取ったので諦めてexpectを使いました。。。(分かる人いたら教えてください)
1Passwordのロック解除に使うパスワードはスクリプト実行時の引数として渡す想定です。結局このパスワードの管理という新しい問題が出てくるのですが、今は1PasswordのパスワードをRundeckのKey Storageに入れて、Rundeckからスクリプトを実行するようにしています。

import { execSync } from 'child_process';

const opPassword = process.argv[2];
const opSession = execSync(`expect -c 'spawn op signin --raw; expect -re Enter; send -- "${opPassword}\\r"; expect EOF exit 0'`, { shell: '/bin/bash' }).toString().split(/[\r\n]+/)[2];

1Passwordから認証情報を取得する

↑で取得したセッショントークンを使って以下のように認証情報を取得できます。

// 1Password上の全アイテムを取得
const items = JSON.parse(execSync(`op list items --session ${opSession} --categories Login`).toString());

// 目的のアイテムを探し、アイテムのuuidを取得
const uuid = items.filter(e => e.overview.title === '目的のアイテムのタイトル')[0].uuid;

// ユーザ名を取得
const username = execSync(`op get item ${uuid} --session ${opSession} --fields username`).toString();

// パスワードを取得
const password = execSync(`op get item ${uuid} --session ${opSession} --fields password`).toString();

// ワンタイムパスワードを取得(標準出力そのままだと改行が含まれてしまうので除去)
const otp = execSync(`op get totp ${uuid} --session ${opSession}`).toString().replace(/[\r\n]/g, '');

Puppeteerでログインする

認証情報が取得できるところまで行ったら、あとはPuppeteerに頑張ってもらうだけです。

// Puppeteerの準備
import * as puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
            args: ['--no-sandbox', '--disable-setuid-sandbox'],
            headless: true,
        });
const page = await browser.newPage();
page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36");
const gotoOpt = { waitUntil: ['load', 'networkidle0', 'domcontentloaded'] };

// ログインボタン等をクリックして次の画面に遷移するのに以下のような関数を用意しておくと便利
async function clickAndWaitForNavigation(selector) {
        const wait = page.waitForNavigation();
        const elem = await page.$(selector);
        await elem.click();
        await wait;
}

// ログインページを開く
await page.goto('ログインページのURL', gotoOpt);

// ユーザ名を1Passwordから取得して入力
const username = execSync(`op get item ${uuid} --session ${opSession} --fields username`).toString();
await (await page.$('input[type=email]')).type(username); // セレクタは適宜変更してください

// パスワードを1Passwordから取得して入力
const password = execSync(`op get item ${uuid} --session ${opSession} --fields password`).toString();
await (await page.$('input[type=password]')).type(password); // セレクタは適宜変更してください

// ↑で定義した関数を使ってログインボタンをクリックし、ワンタイムパスワード入力画面に遷移
await clickAndWaitForNavigation('input[type=submit]'); // セレクタは適宜変更してください

// ワンタイムパスワードを1Passwordから取得して入力
const otp = execSync(`op get totp ${uuid} --session ${opSession}`).toString().replace(/[\r\n]/g, '');
await (await page.$('input[type=tel]')).type(otp); // 今回試したサービスではなぜか`type=tel`だった。セレクタは適宜変更してください

// もう一度ログインボタンをクリック
await clickAndWaitForNavigation('input[type=submit]'); // セレクタは適宜変更してください

あとは目的のページに移動して煮るなり焼くなりするだけなのですが、この状態だとサービスによってはスクレイピングバッチ処理を動かすたびにログインしたよメールが届くので、Cookieを保管してログインを省略するようにしたほうがいいかもしれません。

ログイン後にCookieを保存し、次回以降のログインを省略する

// Cookieの保存先
import * as fs from 'fs'
const cookie_path = 'puppeteer/cookie.txt;

// Puppeteerの準備(省略)

// Cookieのファイルが存在していれば内容をpageにセットする
if (fs.existsSync(cookie_path )) {
    const savedCookies = JSON.parse(fs.readFileSync(cookie_path, 'utf-8'));
        for (let cookie of savedCookies ) {
            await page.setCookie(cookie);
        }
}

// ログアウトのリンクが存在しているか否かによってログイン状態を判定する関数を作成
async function isNotLoggedIn() {
    const logoutLink = await page.$('a[href="/sign_out"]'); // セレクタは適宜変更してください
    return !logoutLink;
}

// スクレイピングしたいページに移動
await page.goto('目的のURL');

if (isNotLoggedIn()) {
    // ログイン処理(省略)
    // ログイン後にCookieを保存
    const cookies = await page.cookies();
    fs.writeFileSync(cookie_path , JSON.stringify(cookies));
}

// スクレイピングの処理

iPhoneのショートカットで定形メールを作成する

背景

毎日終業後に社用のiPhoneから日報をメール送信しているが、定形メールを送る機能が標準メールアプリにもOutlookにもなく、過去メールからのコピペによる非効率なメール作成を強いられていた。
耐えかねてiPhone用の定形メール送信アプリを探していたが、下記の要件に対応できるものが見つからなかった。

  • タイトルに日付を入れる
  • 定形とはいえ、業務内容など一部項目は日によって異なる内容を書くことができる

自分でアプリ作ったり簡単なHTML書いてmailtoのリンクでゴニョゴニョすることを考えたが、運用込みのコストが高すぎるように思えた。
色々探しているとAppleが出しているショートカットというアプリを使えばコードレスで簡単なプログラム作ってホーム画面のアイコンから実行できるみたいだったので試してみた。
純正アプリなのも会社のインストール禁止アプリに引っかからなくて使いやすいポイント。

ショートカット

ショートカット

  • Apple
  • 仕事効率化
  • 無料

ショートカット自体の説明はググれば出てくるけど、エンジニアならあんまり調べる必要もないかもしれない。
当方はソフトウェアエンジニア経験3年弱でブランク1年弱だけど何も調べなくても1時間ぐらい弄っていたらだいたい期待通りのものが作れた。

やり方

テキストやらフォーマットした日付やらの組み合わせでメールのタイトルやら本文やらを作り、 ms-outlook://compose?to=宛先&cc=写し&subject=題名&body=本文 という形式でOutlookURI Schemeを作って開く感じにした。
URLエンコード という部品があり、それを使えば日本語とか改行とか悩まずに済む。

スクリーンショット貼り付けるのも微妙なので表形式でステップを書き出してみる。

部品
(正式な呼び名知らん)                                
パラメータ名                                 パラメータ値                                 コメント
テキスト テキストの内容 <自分の名前> 同僚に配れるように名前の入力を先頭に切り出して改変しやすくした
変数に追加 変数名 名前
テキスト テキストの内容 現在の日付 名前 日報 現在の日付というプリセットの変数があり、フォーマットも指定できる。今回はカスタムフォーマットでM/dにした
URLエンコード モード エンコード 常にこれをやっておくのが無難
変数に追加 変数名 題名
テキスト テキストの内容 nippou-teishutsu-saki@my-company.com
変数に追加 変数名 TO
テキスト テキストの内容 各位

お疲れ様です。名前です。
本日の日報です。

URLエンコード モード エンコード 常にこれをやっておくのが無難
変数に追加 変数名 本文
テキスト テキストの内容 ms-outlook://compose?to=TO&subject=題名&body=本文
URLを開く Safariで開けばOutlookが立ち上がる

これを日報っていう名前で作成してホーム画面に配置すれば、日報アイコンをタップするだけであらかた出来上がった状態でメール送信画面が立ち上がる。

課題

これを同僚に簡単に配布できるようにしたいんだけど、ファイルとして共有してOutlookで送ったやつをショートカットアプリで読み込むと壊れているとか言われる。
メールアドレスとか入ってるからiCloudで公開できないし、どうしたらいいのか分からない。。

雑記欄があるんだけど、天気の情報を取得して「今日はいい天気だった」とかうまいこと自動生成できないだろうか。日報ってまじで不毛だ。

htmlとjsで1歳の子供が遊べるおもちゃを作った話

背景

最近、僕がリビングでPCを触っていると1歳3ヶ月になる子供がキーボードを的確に押してきて作業の邪魔をするようになった。
PCに興味を持つのはいいんだけど、子供がキーを押すと僕の作業の効率が著しく落ちる割に、「エディタ画面で文字がいくつか増える」っていう体験から子供が受ける刺激があんま大したことないのが気に入らなかった。そもそもたぶん僕のまねをするのが嬉しいだけで文字が増えてるの気づいてないし。
そこで、子供のランダムなキータイプに対してちょっと面白げな反応が返ってくるおもちゃをHTMLとJavaScriptで作ってみた。

ちなみに年初にエンジニアからコンサルタントに転身して半年ぐらい全然コード書いてなかったんだけど、以前なら30分ぐらいでできてたぐらいのものを作るのに2時間かかった。。
定期的になにか作らねば作り方忘れてしまうね。

成果物

f:id:miyataro32:20190711214915g:plain

  • キーを押すたびにパステルカラーの●▲■がランダムに画面上に出る
  • 数字キーを押すと図形が押した数字分一気に出る
  • エンターキーまたはスペースキーの押下でア○パンマンかバイキ○マンが出る

github.com

一応作り方

全くたいしたことないけど一応作り方など。

現役時代はvue勉強してたんだけど、めんどかったのでjquery使った。

図形

図形は全部div要素で作りました。大きさと色はランダムにしたかったので%color%とか%size%とかしてあとから置き換えた。
triangleは正三角形にしたかったのでstyle属性の中でcalcで√3掛けたりして計算してる。

const shapes = {
    circle: '<div class="shape circle" style="width: %size%px;height: %size%px;border-radius:50%;background: %color%;" />',
    rectangle: '<div class="shape rectangle" style="width: %size%px;height: %size%px;background: %color%;" />',
    triangle: '<div class="shape triangle" style="border-color: transparent transparent %color% transparent;border-style: solid;border-width: 0 calc(%size%px / 2) calc(%size%px * 1.7320508 / 2) calc(%size%px / 2);height: 0;width: 0;" />'
};

図形の描画

こんな感じの関数を作って引数にランダムな色やらサイズやらを入れて描画している。

const objectDrawer = {};
objectDrawer.drawShapeWithDom = function (shape, size, color, position_x, position_y) {
    let obj = $(shapes[shape].replace(/%color%/g, color).replace(/%size%/g, size))
        .css('left', 'calc(' + position_x + '% - ' + size / 2 + 'px)')
        .css('top', 'calc(' + position_y + '% - ' + size / 2 + 'px)')
        .css('position', 'absolute')
        .appendTo($('body'))
        .show()
        .fadeOut(3000);
    window.setTimeout(() => obj.remove(), 3000);
};

jqueryfadeOutを使って消えるようにしてみた。
fadeOutしてもDOMが残り続けるのが嫌だったのでsetTimeoutでfadeOutしたあとのDOMは消してます。

キーイベントとの紐づけ

普通にonkeypressでランダムな図形かア○パンマン表示するだけ。

(少なくとも)Chromeだとキーを押しっぱなしにすると連続してkeypressとkeydownイベントが発火するみたいだったので、一回のキー押下では一回しか図形が出ないようにkeyIsBeingPressedという変数で押しっぱなしかどうか判断するようにしてみた。

let keyIsBeignPressed = false;
window.onkeypress = function (e) {
    if (keyIsBeignPressed) {
        return;
    } else {
        keyIsBeignPressed = true;
    }
    switch (e.code) {
        case 'Enter':
        case 'Space':
            objectDrawer.drawAnpanman();
            break;
        default:
            if (/Digit|Numpad/.test(e.code)) {
                objectDrawer.drawRandomShapes(e.code.replace(/Digit|Numpad/, ''));
            } else {
                objectDrawer.drawRandomShape();
            }
    }
    console.log(e.code);
};
window.onkeyup = function (e) {
    keyIsBeignPressed = false;
};

最後に

まだユーザ試行をやっていないので、今週末辺りにエンドユーザに触ってもらって、あーとかうーとかいうフィードバックを貰おうかと思っています。
平日の僕が帰る時間にはエンドユーザが寝てるので。
ユーザに喜んでもらえるように継続的にギミック仕込んで改良していく予定です。

なんだか初心者が初めてjs書いてみましたみたいな感じになってるけど、実は俺初心者じゃないんだぜ。。。
そのうち生活の役に立つでかめのアプリを真面目に作る予定です。構想だけはあります。

macOSにzabbix-agentを入れる

MacBook Proにzabbix-agentを入れようとしたらやたらハマったのでメモ。

参考にしたサイト

komaken.me

baqamore.hatenablog.com

nonsensej.xyz

ハマった内容

zabbix_agentdを直接叩けば起動ができてzabbix-serverとの疎通も取れたが、OS起動時等にlaunchdから自動起動することができない。

結論

zabbix_agentdをシェルスクリプトでラップしてlaunchdから呼んだらなぜか普通に動いた。

顛末

  • brew install zabbix --without-server-proxyでzabbix-agentをインストールして起動及びサーバとの疎通確認!楽勝だね!
  • 自動で起動するために/System/Library/LaunchDaemons/com.zabbix.zabbix_agentd.plistを下記のように作った!まだまだ余裕!
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.zabbix.zabbix_agentd</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
    <key>Program</key>
    <string>/usr/local/sbin/zabbix_agentd</string>
</dict>
</plist>
  • sudo launchctl load /System/Library/LaunchDaemons/com.zabbix.zabbix_agentd.plistするも何故か起動しない。。
  • plistに下記の行を加えてログを出そうとしてみた。outとerrのファイルは作られるのに中身は空。/var/log/system.logにも何も手がかりが出ない。。。
   <key>StandardOutPath</key>
   <string>/tmp/zabbix_agentd.out</string>
   <key>StandardErrorPath</key>
   <string>/tmp/zabbix_agentd.err</string>
  • 下記のようなシェルスクリプトを作り、plistのProgramを置き換えてloadしてみたらちゃんとoutにrootと出力された。
#!/bin/bash
whoami
  • brewでインストールしたzabbix-agentがおかしいんじゃね!?と思ったのでアンインストールしてソースからビルドした。とりあえず最新版の4.0.1を入れた。ここはここで若干ハマったのでちょっと詳しく書いておく。
    • バージョン以外はMacにZabbixエージェントを入れるにある通りに進めたが、./configure --enable-agentしたらiconvが無いと怒られた。
    • brew install libiconvしたあとでもう一度./configure --enable-agentしたが、またiconvが無いと怒られて困った。パスも通ってるのに意味が分からないよ。。。
    • configureの中身を見てみたら--with-iconvというオプションがあるらしいので、./configure --enable-agent --with-iconv=/usr/local/Cellar/libiconv/1.15/bin/iconvとしてみたら通った。
    • make installしてビルド。
  • 起動及びサーバとの疎通確認!launchdから起動しようとしたら失敗!ログも出ない!!
  • 下記のようなシェルスクリプト/usr/local/sbin/zabbix_agentd.shを作ってzabbix_agentdをラップしてみた。
#!/bin/bash

echo "executed by $(whoami) at $(date)"

zabbix_agentd=/usr/local/sbin/zabbix_agentd

echo "call ${zabbix_agentd}"
${zabbix_agentd}
  • plistのProgramを書き換えてsudo launchctl load /System/Library/LaunchDaemons/com.zabbix.zabbix_agentd.plistしたら動いた!何でだろう!!
  • MacBookを再起動してps -el | grep zabbixとしたところ、意図したとおり自動起動されていた。

つまりこうしたらできるらしい

  • zabbix-agentをインストールする。多分brewでもなんでもいい。
  • /usr/local/sbin/zabbix_agentd.shを作成して実行権限を付与する。
#!/bin/bash

echo "executed by $(whoami) at $(date)"

zabbix_agentd=/usr/local/sbin/zabbix_agentd

echo "call ${zabbix_agentd}"
${zabbix_agentd}
  • /System/Library/LaunchDaemons/com.zabbix.zabbix_agentd.plistを作成してオーナーをroot:wheelにする。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.zabbix.zabbix_agentd</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
    <key>StandardOutPath</key>
    <string>/tmp/zabbix_agentd.out</string>
    <key>StandardErrorPath</key>
    <string>/tmp/zabbix_agentd.err</string>
    <key>Program</key>
    <string>/usr/local/sbin/zabbix_agentd.sh</string>
</dict>
</plist>
  • sudo launchctl load /System/Library/LaunchDaemons/com.zabbix.zabbix_agentd.plist