rsync ห้ามหยุด ห้ามหายและนายต้องไปต่อ

บล็อกต้อนรับต้นเดือนและวันแรงงาน ด้วยจำเป็นต้องย้ายไฟล์จำนวนน้อยนิดมหาศาล แบบเร่งด่วน จำนวนหนึ่งไปไว้อีกทีและงานนี้ก็ห้ามหยุดจนกว่างานจะเสร็จ (นี่มันสะท้อนชีวิตใครบ้างไหม ^__^) ใครมีงานแบบนี้ต้องทำแบบด่วน ๆ และไม่มีเวลาให้ต้องมามอนิเตอร์และรันใหม่อีกรอบ ก็ใช้สคริปท์นี้ไปปรับได้

อธิบายคร่าว ๆ คือ

  • ใช้ rsync ในการโอนย้ายไฟล์ไปอ่านรายละเอียดต่อได้ที่นี่
  • อาจมีบางช่วงหรือ error ขัดจังหวะช่วงโปรแกรม rsync ทำงานแล้วโปรแกรมปิดตัวลงก็ให้โปรแกรม rsync กลับมาทำงานต่อ (Loop) จนกว่าจะสำเร็จ (ดูบังคับมากมาย)
  • เสร็จแล้วก็ Line notify นิดนึง
#!/usr/bin/env bash

# Declare an array of Token
declare -a ACCESS_TOKENS=("token1" "token2")
START=$(date +"%d-%m-%Y %H:%M")
RC=1

while [[ $RC -ne 0 ]]
do
    NOW=$(date +"%d-%m-%Y %H:%M")

    rsync -arv --partial --progress --ignore-errors [source] [destination]
    RC=$?
    
    if [[ $RC -ne 0 ]] ; then
        echo "$NOW Rsync failure. Backing off and retrying..."
        sleep 180
    fi
done

# Iterate the token array
for token in "${ACCESS_TOKENS[@]}"; do
    echo $token
    curl -X POST https://notify-api.line.me/api/notify -H "Authorization: Bearer $token" -F "Rsync start $START completed normally" > /dev/null 2>&1   
done

echo "Rsync start $START completed normally"

จบปิ๊งงง (ไม่ได้ดื้อก็แค่พยายาม)
ป.ล.
เผื่อเคสต้องรีสตาร์ทก็เอาไปไว้ใน cron job ด้วยนะ ^__^

 

Screenshot หน้าเธอแล้วส่งเข้า Line Notify กัน

ปฏิเสธไม่ได้ว่าแอปลิเคชั่นไลน์มีผลอย่างมากกับแวดวงคนทำงาน จะเอกชนหรือวงราชการก็ต้องใช้ แต่ด้วยความที่แอปลิเคชั่นสื่อสารแบบนี้การส่งข้อความยาว ๆ บางครั้งก็เล่นเอาหัวเสียเหมือนกัน เพราะใจความสำคัญมันถูกเผยแพร่แต่เพราะข้อความมันยาว จึงขาดความน่าสนใจ คนรับก็เลื่อนผ่านไปโดยปริยาย (เหมือนเธอที่มองผ่านฉันไป เอ้าเพลงมาาา ซูดดดดด)

รวมทั้งข้อมูลรายงานผลโรค COVID-19 ที่ตอนนี้หลาย ๆ สื่อหลาย ๆ ที่ ได้ทำย่อยข้อมูลเหล่านี้ออกมาเป็น Infographic บ้าง เว็บไซค์แสดง Dashboard บ้าง งั้นจะช้าอยู่ไย เราเซฟรูปเหล่านั้นแล้วส่งเข้าไลน์เลยดีกว่า

Shell Script ไม่ได้มีอะไรซับซ้อนใช้ Headless Chrome ให้ Screenshot หน้าจอตอนนั้นแล้วส่งเข้าไลน์ก็เป็นอันเสร็จสิ้น

#!/bin/bash
  
PICTURE_PATH="./screenshot.png"
NOW=$(date +"%d-%m-%Y %H:%M")
IP=$(hostname -I | awk '{ print $1 }')
DASHBOARD="IP Address"

# Declare an array of Token PHP-CC PSH
declare -a ACCESS_TOKENS=("token1" "token2")

command_exists()
{
  command -v "$1" >/dev/null 2>&1
}

# Take pictures using Chrome Headless
if command_exists google-chrome ; then
    google-chrome --headless --no-sandbox --screenshot --hide-scrollbars --virtual-time-budget=10000 --disable-gpu --window-size=680,380 http://$IP:88/covid
    convert -resize 1280x screenshot.png screenshot-01.png

    google-chrome --headless --no-sandbox --screenshot --hide-scrollbars --virtual-time-budget=10000 --disable-gpu --window-size=1280,1410 http://$DASHBOARD
    mv screenshot.png screenshot-02.png
else
    if command_exists docker ; then 
        sudo docker container run -it --rm -v $(pwd):/usr/src/app zenika/alpine-chrome --no-sandbox --screenshot --hide-scrollbars --virtual-time-budget=10000 --disable-gpu --window-size=680,380 http://$IP:88/covid
        convert -resize 1280x screenshot.png screenshot-01.png

        sudo docker container run -it --rm -v $(pwd):/usr/src/app zenika/alpine-chrome --no-sandbox --screenshot --hide-scrollbars --virtual-time-budget=10000 --disable-gpu --window-size=1280,1410 http://$DASHBOARD
        mv screenshot.png screenshot-02.png
    else
        echo 'Not found docker'
    fi  
fi

convert -append screenshot-01.png screenshot-02.png screenshot.png

# Iterate the token array
for token in "${ACCESS_TOKENS[@]}"; do
    echo $token
    # curl -X POST https://notify-api.line.me/api/notify -H "Authorization: Bearer $token" -F "message=รายงานสถานการณ์ COVID-19 ณ $NOW" -F "imageFile=@$PICTURE_PATH" > /dev/null 2>&1   
done

ผลลัพธ์ที่ได้ก็ตามนี้

จบปิ๊ง บล๊อกเดือนนี้ ^__^
ป.ล.

  • เราใช้ Dashboard ของ อ.เอ แห่ง Developer Mate
  • มี Dashboard 2 ตัว ก็เลย Merge ไฟล์รวมกันซะจะทิ้งไปก็เสียดาย
  • Token ของ Line Notify น่าจะมีกันแล้วนะ อันนี้ข้ามเรื่องการสร้าง Line Notify Token หล่ะกัน
  • ตัวเลขผู้ติดเชื้อวันนี้เห็นแล้วมีแรงทำงานสู้กันต่อ

ลบแถวที่ซ้ำกันใน MySQL

เมื่อมีข้อมูลซ้ำเกิดขึ้นในตารางจะด้วยสาเหตุอะไรก็แล้วแต่ แต่มันมีข้อมูลซ้ำเกิดขึ้นแล้วและเราต้องการลบตัวที่ซ้ำออกไป เราจะทำยังไงมาดูแนวทางกัน

เริ่มต้นเราดูตัวอย่างข้อมูลที่ซ้ำกันก่อน

DROP TABLE IF EXISTS contacts;
 
CREATE TABLE contacts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL, 
    email VARCHAR(255) NOT NULL
);
 
INSERT INTO contacts (first_name,last_name,email) 
VALUES ('Carine ','Schmitt','carine.schmitt@verizon.net'),
       ('Jean','King','jean.king@me.com'),
       ('Peter','Ferguson','peter.ferguson@google.com'),
       ('Janine ','Labrune','janine.labrune@aol.com'),
       ('Jonas ','Bergulfsen','jonas.bergulfsen@mac.com'),
       ('Janine ','Labrune','janine.labrune@aol.com'),
       ('Susan','Nelson','susan.nelson@comcast.net'),
       ('Zbyszek ','Piestrzeniewicz','zbyszek.piestrzeniewicz@att.net'),
       ('Roland','Keitel','roland.keitel@yahoo.com'),
       ('Julie','Murphy','julie.murphy@yahoo.com'),
       ('Kwai','Lee','kwai.lee@google.com'),
       ('Jean','King','jean.king@me.com'),
       ('Susan','Nelson','susan.nelson@comcast.net'),
       ('Roland','Keitel','roland.keitel@yahoo.com');

มาดูชุดข้อมูลที่ซ้ำกันบ้างว่ามีแถวใดบ้างที่ซ้ำกัน

SELECT 
    email, 
    COUNT(email),
    GROUP_CONCAT(id) AS id_list
FROM contacts
GROUP BY email
HAVING COUNT(email) > 1;


ในแถวที่ซ้ำกันเรา และอยากเก็บไว้แค่ 1 แถว
วิธีที่ 1
ทำการลบด้วยคำสั่งดังนี้

DELETE c1 FROM contacts c1
INNER JOIN contacts c2 
WHERE c1.id > c2.id AND c1.email = c2.email;

จากคำสั่งข้างบนหมายความว่าเมื่อมีแถวที่ซ้ำกันให้เก็บไว้เฉพาะแถวแรกที่พบ (id ที่ค่าน้อยกว่า) ซึ่งผลลัพธ์ได้ก็ประมาณนี้ (id 6, 12, 13, 14 จะถูกลบไป)

วิธีที่ 2

ALTER IGNORE TABLE contacts 
ADD UNIQUE INDEX idx1(email);

ผลลัพธ์ที่ได้ก็เป็นเช่นคำสั่งด้านบน แต่ขอ หมายเหตุไว้หน่อยเนื่องจาก ALTER IGNORE TABLE ถูกยกเลิกไปแล้วใน MySQL เวอร์ชั่น 5.7.4 เป็นต้นไปวิธีนี้ก็เลยใช้ได้เฉพาะเวอร์ชั่นที่ก่อนหน้าที่ระบุนะครับ

วิธีที่ 3

เนื่องจากหมายเหตุในวิธีที่ 2 ใน MySQL เวอร์ชั่นใหม่ ๆ ก็เลยใช้วิธีแบบนี้แทน

CREATE TABLE contacts_copy LIKE contacts;
ALTER TABLE contacts_copy ADD UNIQUE INDEX idx1(email);

INSERT IGNORE INTO contacts_copy
SELECT * FROM contacts;

DROP TABLE contacts;
ALTER TABLE contacts_copy RENAME TO contacts;

จบปิ๊ง ๆ
ไปลองทดสอบกันได้ที่ https://www.db-fiddle.com/f/sXB3sBDQEdrbdEDFi98Y3j/0
**ส่วนความเจ็บปวดซ้ำ ๆ หน่ะมันลบไม่ได้หรอกนะ

MySQL Get First value or Last value in each group

ใน MySQL ตั้งแต่เวอร์ชั่น 8.0 เป็นต้นไป ถ้าต้องการดึงค่าแรกและค่าสุดท้ายในกรุ๊ปนั้น ๆ ออกมา สามารถใช้ Function

  • FIRST_VALUE()
    FIRST_VALUE(expr) [null_treatment] over_clause
    Returns the value of expr from the first row of the window frame.
  • LAST_VALUE()
    LAST_VALUE(expr) [null_treatment] over_clause
    Returns the value of expr from the last row of the window frame.

ตัวอย่างการใช้งาน

โจทย์ มีข้อมูล CKD Stage (สมมุติ) เรียงตามรายปีงบประมาณ ถ้าต้องการทราบ Stage แรกและสุดท้ายที่ตรวจพบในรายนั้น ๆ หาได้จาก

เริ่มต้นคือสร้างตารางทดสอบ

CREATE TABLE ckdstage(vn INT(11) AUTO_INCREMENT,
    hn VARCHAR(50) NOT NULL,
    fiscal_year INT NOT NULL,
    stage INT(1) NOT NULL,
    PRIMARY KEY(vn)
);
 
INSERT INTO ckdstage(vn,hn,fiscal_year,stage)
VALUES
(NULL,'1945',2016, 4),
(NULL,'1945',2017, 3),
(NULL,'1945',2018, 5),
(NULL,'1945',2019, 3),
(NULL,'2311',2016, 4),
(NULL,'2311',2017, 5),
(NULL,'2311',2018, 3),
(NULL,'2311',2019, 2);

ข้อมูลที่ได้จะเป็นตามนี้ ถ้ามองด้วยสายตาค่าที่เราต้องการจะตามที่ลูกศรชี้

หาผลลัพธ์ตามโจทย์

SELECT 
    hn,
    fiscal_year,
    FIRST_VALUE(stage) OVER (
      	PARTITION BY hn
        ORDER BY fiscal_year
    ) first_stage,	
    LAST_VALUE(stage) OVER (
        PARTITION BY hn      
        ORDER BY fiscal_year
        RANGE BETWEEN
            UNBOUNDED PRECEDING AND
            UNBOUNDED FOLLOWING
    ) last_stage
FROM ckdstage;

จะเห็นได้ว่าผลลัพธ์ทีได้จะดึงค่าแรกสุดและค่าสุดท้ายตามกรุ๊ปของคอลัมภ์ hn แต่ข่าวร้ายก็คือฟังก์ชั่นนี้ไม่มีให้ใช้ใน MySQL เวอร์ชั่นเก่า 55555 ซึ่งก็สามารถแก้ปัญหาโดยการจำลองฟังก์ชั่น First(), Last()  ขึ้นมาใช้งานเอง ตัวอย่าง

SELECT 
    t1.hn,
    GROUP_CONCAT(t1.stage ORDER BY t1.vn ASC) AS stagelist_asc,
    GROUP_CONCAT(t1.stage ORDER BY t1.vn DESC) AS stagelist_desc,
    SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(t1.stage ORDER BY t1.vn ASC), ',', 1) , ',' , -1) AS first_stage,
    SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(t1.stage ORDER BY t1.vn DESC), ',', 1) , ',' , -1) AS last_stage
FROM ckdstage AS t1
GROUP BY t1.hn

จบปิ๊ง !! ^__^

ป.ล.

 

มาลองรัน KDE neon บน Docker กัน

ก่อนอื่นสำหรับใครที่ไม่คุ้นกับ KDE

KDE หรือชื่อเต็ม K Desktop Environment เป็นสภาพแวดล้อมในการทำงานแบบเดสก์ท็อป (Desktop Environment) ที่เป็นซอฟต์แวร์เสรี พัฒนาบนทูลคิท Qt ของบริษัท Trolltech และทำงานได้บนระบบปฏิบัติการตระกูลยูนิกซ์เกือบทุกรุ่น เช่น ลินุกซ์, BSD, AIX และ Solaris รวมถึงมีรุ่นที่ใช้งานได้บน Mac OS X และไมโครซอฟท์วินโดวส์

ปัจจุบัน (22 มกราคม 2561) นี่ก็พัฒนา Plasma รุ่นเสถียรถึงเวอร์ชั่น 5.12.7 (LTS) ละ  KDE เป็น Desktop Environment เพราะฉะนั้น เราก็สามารถติดตั้งบน Distributions ต่าง ๆ ได้ เช่น CentOS Debian Ubuntu หรือแม้กระทั่ง Windows เองก็เหอะ

แล้ว KDE neon หล่ะมายังไง

KDE neon is a set of software repositories for Ubuntu long-term support (LTS) releases with latest 64-bit version of KDE desktop and applications. The KDE neon Linux distribution is focused on the development of KDE

ด้วยความที่ neon รันอยู่บน Ubuntu เราก็สามารถโหลดไฟล์ ISO มารัน/ติดตั้งใน VMWare หรือ VirtualBox ได้เลย

รันบน VertualBox Version 6.0.2

 

หน้าสำหรับดาวน์โหลด Live Images

เลื่อนลงมานิดนึงก็จะมีเวอร์ชั่นสำหรับ Docker ให้ใช้งานด้วย จุดประสงค์ของคนที่ใช้งานบน Docker หลัก ๆ ก็น่าจะมาจาก

  • อยากลอง
  • ลองเสร็จก็พอ อยากลบทิ้งแบบคลีน ๆ
  • VMWare/Virtual Box  หน่วงรู้สึกได้
  • Docker มันก็ทำได้นี่นา งั้นก็ลองกันเลย

เริ่มด้วยกันติดตั้ง Docker (สำหรับใครที่ยังไม่ได้ติดตั้ง ปกติแล้วเรามักจะอ้างอิงจาก เอกสารทางการของทาง Docker เอง นั่นชัวร์ที่สุดละ)

sudo apt-get install docker-ce xserver-xephyr

** xserver-xephyr ต้องติดตั้งเพิ่มด้วยนะ

เสร็จก็ทำการ pull image ล่าสุดของ KDE neon

sudo docker pull kdeneon/plasma:dev-unstable

ขนาดก็ประมาณ 1 GB สายเปรี้ยวก็ใช้เวอร์ชั่น dev-unstable ไป ^__^
ถึงขั้นตอนการรันก็ใช้คำสั่งตามนี้  (เป็นค่าเริ่มต้น) เพื่อให้ Xephyr ทำหน้าที่เป็น X Server Window

Xephyr -screen 1024x768 :1 &
docker run -v /tmp/.X11-unix:/tmp/.X11-unix kdeneon/plasma:dev-unstable

จบปิ๊ง ^__^

เพิ่มเติม

ติดตั้ง Traccar แพลตฟอร์มสำหรับ ระบบ GPS Tracking บน Digital Ocean

Traccar is a free and open source modern GPS tracking system

Traccar เป็น GPS Tracking Platform หมายความว่า มีระบบและบริการที่สนับสนุนเช่น Server Client และระบบจัดการ (Manager) เสร็จสรรพ

โดยตัว Server เองก็รองรับทั้ง Windows Linux และ Arm ตอนนี้เป็นเวอร์ชั่น 4.2 ส่วน Client ก็มีทั้ง Android iOS ส่วนอุปกรณ์ GPS อื่นที่รองรับก็เข้าไปดูที่นี่เลย

ส่วน GPS Tracking คือการระบุตำแหน่งของวัตถุผ่านระบบระบุตำแหน่งบนพื้นโลก (Global Positioning System: GPS) ซึ่งใช้เพื่อติดตามและระบุตำแหน่งของวัตถุนั้นๆจากระยะไกล โดยเทคโนโลยี GPS tracking นี้สามารระบุได้ครอบคลุมถึงพิกัดภูมิศาสตร์ ละติจูด, ลองจิจูด, ความเร็วบนภาคพื้น ทิศทางและเส้นทางการเคลื่อนที่ของวัตถุนั้นๆ ที่เราติดตามอยู่ได้

เพราะฉะนั้นระบบนี้ก็ถูกนำมาใช้ในงานหลาย ๆ งาน ที่เราเห็นกันบ่อย ๆ คือระบบติดตามตำแหน่งรถยนต์หรือยานพาหนะนั่นแหล่ะ ไม่ว่าจะเป็นรถขนส่ง รถฉุกเฉินในระบบสาธารณสุข

ซึ่ง Traccar เองก็มีทั้งเสียเงินและฟรีราคาให้บริการก็ตามนี้แต่ที่เราสนใจคือตัวที่เป็น Opensource

เริ่มกันด้วยการติดตั้งตัว Server โดยความเป็นจริงละ คนที่จะใช้งานจำเป็นต้องมี Server ที่ให้บริการจริง ๆ ในระบบอินเตอร์เนตเพื่อให้อุปกรณ์ GPS ติดต่อได้ อาจเป็นเครื่องจริง ๆ วางไว้ที่ IDC สักที่หรือถ้าไม่ลงทุนเครื่อง Server จริง ๆ จะเป็น VPS (Virtual Private Server) ก็ได้ ในท้องตลาดก็มีทั้งของไทยและต่างประเทศที่ดัง ๆ หน่อยก็เป็น Amazon Digital Ocean ในบล๊อกนี้เราใช้ DigitalOcean ตัวเล็กสุด 1 CPU SSD 25GB ค่าบริการ 5$/month สร้างตัว Droplet ขึ้นมา รายละเอียดเรื่อง VPS อ่านได้ที่นี่ครบละสำหรับคนเริ่มต้น ส่วนใครจะสมัครตอนนี้ไปนี่เลย

https://m.do.co/c/2f653359b82c
**ตอนนี้สมัครตาม Referral ก็จะได้เครดิตไว้ใช้งาน 100$

หลังจากที่สร้าง Droplet เสร็จแล้ว (ในที่นี้ใช้ Ubuntu 18.10) ก็ติดตั้ง Docker กันต่อเลย (Traccar มี Docker Image ให้ด้วย ^__^)

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

แล้วก็เพิ่ม Docker repository เข้าไป (ตอนนี้ Docker สำหรับ Ubuntu 18.10 ยังไม่มีรุ่น Stable ออกมานะ ต้องใช้ รุ่น test ไปพลาง ๆ )

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) test"

เสร็จแล้วก็ทำการ Update และติดตั้ง Docker ต่อได้เลย

sudo apt-get update
sudo apt-get install -y docker-ce

ถ้าไม่มีอะไรผิดพลาดตัว Docker ต้องทำงานละ

sudo systemctl status docker

ที่นี้ก็เริ่มติดตั้ง Traccar Server กัน ให้ทำการสร้าง Directory ขึ้นมา 2 Directory คือ

  • logs สำหรับเก็บล็อกใช้งาน
  • conf สำหรับเก็บไฟล์ Config
mkdir -p {conf,logs}

ต่อมาก็สร้างไฟล์ Config ในที่นี้ใช้ไฟล์ Default จาก Traccar เลย

docker run \
--rm \
--entrypoint cat \
traccar/traccar \
/opt/traccar/conf/traccar.xml > conf/traccar.xml

โดยค่ามาตรฐานของ Traccar จะใช้ฐานข้อมูลของ hbase ถ้าเราอยากเปลี่ยนเป็น MySQL หรือตัวอื่น ก็แก้ไขตามรายละเอียดนี้  ไฟล์ Config จะหน้าตาประมาณนี้

<?xml version='1.0' encoding='UTF-8'?>

<!DOCTYPE properties SYSTEM 'http://java.sun.com/dtd/properties.dtd'>

<properties>

    <entry key='config.default'>./conf/default.xml</entry>

    <!--

    This is the main configuration file. All your configuration parameters should be placed in this file.

    Default configuration parameters are located in the "default.xml" file. You should not modify it to avoid issues
    with upgrading to a new version. Parameters in the main config file override values in the default file. Do not
    remove "config.default" parameter from this file unless you know what you are doing.

    For list of available parameters see following page: https://www.traccar.org/configuration-file/

    -->

    <entry key='database.driver'>org.h2.Driver</entry>
    <entry key='database.url'>jdbc:h2:./data/database</entry>
    <entry key='database.user'>sa</entry>
    <entry key='database.password'></entry>

</properties>

เสร็จก็เริ่ม Run Server

docker run \
-d --restart always \
--name traccar \
--hostname traccar \
-p 8082:8082 \
-p 5000-5150:5000-5150 \
-p 5000-5150:5000-5150/udp \
-v /root/logs:/opt/traccar/logs:rw \
-v /root/conf/traccar.xml:/opt/traccar/conf/traccar.xml:ro \
traccar/traccar

จะได้หน้าตา Web Management สำหรับจัดการ Traccar โดยเข้าไปที่ http://<IP Address>:8082
** IP Address จะได้มาจาก VPS ที่เราสร้างขี้น

ต่อไปก็ทำการติดตั้ง Client ตอนนี้มีทั้งที่เป็น Android และ iOS รวมทั้งอุปกรณ์ GPS ตัวอื่น ๆ เช็คอุปกรณ์ที่รองรับได้ที่นี่

การใช้งานก็กำหนด IP Address ของเราตรง Server URL ตามฟอร์แมทตัวอย่างได้เลย

จบเรื่องการติดตั้งทั้ง Server และ Client ^_^

ป.ล.

Simulate keyboard input and mouse activity and more on Raspberry Pi

นานมาแล้ว (หรือช่วงนี้ก็มี 555) มีโปรแกรมกลุ่มพวกคลิกโฆษณาอัตโนมัติ ได้รับความนิยมอย่างล้นหลาม ใคร ๆ ก็ทำงานบนอินเตอร์เนตกัน จนมีประโยคสุดฮิตสแปมมาอยู่เนือง ๆ

โดยหลัก ๆ ละโปรแกรมเหล่านี้ก็จะจำลองการทำงานของเมาส์และคีย์บอร์ดแหล่ะ และวันนึงก็ได้ใช้ ด้วยความที่ตัว Raspberry Pi ที่ใช้งานอยู่จำเป็นต้องจำลองการเลื่อน/คลิกเมาส์และใช้คีย์บอร์ดขึ้นมา xdotool ช่วยคุณได้ เริ่มติดตั้งกันเลย

sudo apt-get install xdotool

ตัวอย่างการใช้งานก็ง่าย ๆ สร้างสคริปท์ขึ้นมา อาจจะทำงานผ่าน Crontab ก็ได้ เช่น ต้องการ
Resize all visible gnome-terminal windows

#!/bin/bash

WIDS=`xdotool search --onlyvisible --name "gnome-terminal"`
for id in $WIDS; do
  xdotool windowsize $id 500 500
done

# As of version 2.20100623, you can do this simpler version of above:
xdotool search --onlyvisible --classname "gnome-terminal" windowsize %@ 500 500

จบปิ๊ง   !!

ป.ล.
อยากใช้ก็มีให้ใช้ได้ทันที แบบนี้สิดี
Github : https://github.com/jordansissel/xdotool

VS Code font rendering looks ugly on Linux

source : https://github.com/Microsoft/vscode/issues/35675

  1. Create fontconfig.conf and put this file in /usr/share/code/fontconfig.conf
    <?xml version='1.0'?>
    <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
    <fontconfig>
      <!-- Force RGBA subpixel aliasing to «none» in code and code-insiders -->
      <match target="pattern">
        <or>
          <test name="prgname">
            <string>code</string>
          </test>
          <test name="prgname">
            <string>code-insiders</string>
          </test>
        </or>
        <edit name="rgba" mode="assign">
          <const>none</const>
        </edit>
      </match>
    </fontconfig>
  2. Symlink file to /etc/fonts/conf.d/99-vscode.conf

Dynamic pivoting in MySQL

คิดถึงรายงานคิดถึง Excel 555 … เรื่องความตั้งใจในการเรียน Microsoft Excel นี่มีมาสักพักแล้ว เริ่มด้วยการซื้อหนังสือมาก่อนแต่น่าจะหลายเดือนละ (จนลืมไปเลยว่าซื้อมาตอนไหน บาปมาก) แต่ก็นะความขี้เกียจครอบงำ ช่วงนี้ก็เลยเป็นช่วงอ่านหนังสือ Microsoft Excel แบบจริงจัง (แอบตั้งใจ)
เปิดดูคร่าว ๆ มีบทนึงพูดถึงเรื่องการทำ Pivot Table ในโปรแกรมตระกูลตารางคำนวณเรื่องนี้ทุกตัวในตลาดก็สามารถทำได้ ถือเป็นหนึ่งใน Killer Feature แต่เรื่องนี้แค่ประเด็นนิดหน่อย ^_^

สำหรับคนที่เขียนคำสั่งเรียกดูข้อมูล (SQL) ก็ทำเรื่องนี้อยู่เนือง ๆ หลายครั้งก็ทำแบบฮาร์ดคอร์ ณ ตอนนั้นเลย ตัวอย่างคร่าว ๆ สมมุติเรามีข้อมูลค่าใช้จ่ายภายในบ้านสรุปประมาณนี้ ในฐานข้อมูล (ด้านซ้าย) และต้องการแสดงผลเป็นอีกรูปด้านขวา

SQL แสดงรายงานแบบนี้ อนุมานจากข้อมูลดิบที่เก็บในฐานข้อมูล ก่อนที่เราจะสรุปได้ก็น่าจะมีประมาณนี้ (เราใช้บริการของ generatedata.com) เอาตัวอย่างข้อมูลสัก 100 รายการหล่ะกัน

DROP TABLE IF EXISTS `ExpenseCategory`;
CREATE TABLE `ExpenseCategory` (
  `CategoryId` varchar(255) NOT NULL,
  `CategoryName` varchar(255) default NULL,
  PRIMARY KEY (`CategoryId`)
);

INSERT INTO `ExpenseCategory` (`CategoryId`,`CategoryName`) VALUES ("01","ความบันเทิง"),("02","ของชำ"),("03","ครอบครัว"),("04","การคมนาคมขนส่ง");

DROP TABLE IF EXISTS `Expense`;
CREATE TABLE `Expense` (
  `id` mediumint(8) unsigned NOT NULL auto_increment,
  `CategoryId` varchar(255) default NULL,
  `ExpenseDate` varchar(255),
  `Amount` varchar(100) default NULL,
  PRIMARY KEY (`id`)
) AUTO_INCREMENT=1;

INSERT INTO `Expense` VALUES (1,'04','2018-07-19 02:55:09','83369'),(2,'01','2018-01-17 19:18:21','28001'),(3,'04','2018-06-29 03:32:27','17058'),(4,'04','2018-07-04 17:44:03','64619'),(5,'04','2018-07-22 17:38:56','24050'),(6,'04','2018-08-15 03:43:50','80585'),(7,'03','2018-06-11 23:52:59','79427'),(8,'02','2018-04-29 04:03:38','63177'),(9,'01','2018-09-03 04:02:46','77975'),(10,'04','2018-07-04 21:49:15','61752'),(11,'01','2018-02-21 11:39:34','59245'),(12,'04','2018-01-05 05:42:14','15695'),(13,'02','2018-09-13 09:07:51','22132'),(14,'02','2018-08-12 13:24:09','58156'),(15,'01','2018-08-14 18:34:16','7802'),(16,'01','2018-04-04 04:26:02','44733'),(17,'03','2018-11-03 02:59:01','73158'),(18,'04','2018-01-23 09:59:45','69724'),(19,'02','2018-11-16 07:39:50','98963'),(20,'03','2018-01-04 11:20:59','93659'),(21,'02','2018-08-25 22:01:10','13945'),(22,'04','2018-04-08 22:46:07','90735'),(23,'01','2018-09-13 10:20:59','76856'),(24,'01','2018-09-02 08:45:17','54668'),(25,'03','2018-12-06 12:19:30','45571'),(26,'02','2018-05-27 09:48:40','24694'),(27,'01','2018-04-15 03:01:29','73961'),(28,'01','2018-03-11 06:05:34','53546'),(29,'02','2018-12-27 06:57:17','91375'),(30,'04','2018-02-05 05:14:19','44997'),(31,'02','2018-12-15 02:35:52','54144'),(32,'02','2018-08-01 02:06:37','95898'),(33,'02','2018-02-18 08:50:43','79249'),(34,'02','2018-03-26 04:39:35','66184'),(35,'01','2018-07-26 13:17:03','44670'),(36,'02','2018-12-26 13:50:39','88500'),(37,'04','2018-04-19 20:26:25','31414'),(38,'01','2018-07-28 13:53:24','56802'),(39,'02','2018-04-27 06:18:15','67682'),(40,'01','2018-10-11 12:25:37','98233'),(41,'03','2018-09-28 12:36:05','43118'),(42,'02','2018-08-17 16:06:34','94942'),(43,'02','2018-12-16 16:28:16','25039'),(44,'04','2018-04-14 15:47:06','87368'),(45,'03','2018-11-21 16:53:01','49355'),(46,'04','2018-01-06 17:08:54','63354'),(47,'03','2018-02-02 11:06:33','26540'),(48,'01','2018-10-19 03:50:49','32534'),(49,'01','2018-05-15 23:40:11','58038'),(50,'03','2018-04-28 23:53:24','2915'),(51,'01','2018-03-30 18:57:12','10229'),(52,'03','2018-04-22 07:47:45','20004'),(53,'03','2018-08-16 23:42:08','69185'),(54,'02','2018-03-09 20:38:52','92533'),(55,'02','2018-12-25 23:51:10','66589'),(56,'04','2018-09-22 05:40:21','94759'),(57,'01','2018-06-03 13:24:32','76588'),(58,'03','2018-04-21 07:27:01','66573'),(59,'02','2018-09-25 03:49:02','76607'),(60,'01','2018-08-11 10:48:24','47755'),(61,'04','2018-11-02 02:37:54','69570'),(62,'02','2018-08-31 14:44:15','25560'),(63,'02','2018-03-23 07:24:59','78550'),(64,'04','2018-06-07 17:39:12','18455'),(65,'02','2018-10-31 11:14:30','71227'),(66,'01','2018-10-31 14:23:16','2454'),(67,'04','2018-08-14 21:16:17','54700'),(68,'04','2018-06-05 01:04:59','59554'),(69,'01','2018-01-23 10:33:50','37798'),(70,'04','2018-06-09 12:00:56','59983'),(71,'04','2018-02-19 15:56:33','56754'),(72,'04','2018-12-24 13:14:33','54684'),(73,'02','2018-05-15 23:08:16','66531'),(74,'02','2018-06-25 19:08:32','25407'),(75,'02','2018-11-25 18:53:14','96119'),(76,'01','2018-05-28 02:43:26','30991'),(77,'04','2018-03-18 02:03:29','81845'),(78,'02','2018-02-18 20:23:03','1057'),(79,'01','2018-11-15 16:32:26','52151'),(80,'01','2018-07-25 19:54:37','91069'),(81,'04','2018-04-18 14:02:13','51134'),(82,'02','2018-06-04 07:27:50','4609'),(83,'04','2018-07-07 20:22:38','62669'),(84,'02','2018-08-13 10:40:36','93161'),(85,'03','2018-03-29 12:51:47','75717'),(86,'04','2018-04-03 15:35:12','67327'),(87,'04','2018-02-06 15:56:58','87785'),(88,'01','2018-12-16 04:38:52','34503'),(89,'03','2018-07-25 02:52:09','87144'),(90,'04','2018-08-21 21:48:58','37756'),(91,'01','2018-07-25 20:38:00','5070'),(92,'01','2018-05-23 17:56:31','40138'),(93,'01','2018-01-10 22:47:36','35626'),(94,'01','2018-03-20 22:01:19','94446'),(95,'01','2018-05-20 23:55:32','65411'),(96,'02','2018-08-06 07:52:03','48145'),(97,'02','2018-05-26 22:38:07','17564'),(98,'01','2018-05-31 21:40:52','59694'),(99,'03','2018-03-03 16:47:55','18672'),(100,'01','2018-04-29 06:15:51','52622');

ปกติ(เรา)เขียน SQL เพื่อสรุปค่าใช้จ่ายแยกตามประเภทและรายเดือนตามตารางรูปขวา

SELECT
    CategoryName, 
    SUM(IF(M = 1, TotalAmount, 0 )) AS '1',
    SUM(IF(M = 2, TotalAmount, 0 )) AS '2',
    SUM(IF(M = 3, TotalAmount, 0 )) AS '3',
    SUM(IF(M = 4, TotalAmount, 0 )) AS '4',
    SUM(IF(M = 5, TotalAmount, 0 )) AS '5',
    SUM(IF(M = 6, TotalAmount, 0 )) AS '6',
    SUM(IF(M = 7, TotalAmount, 0 )) AS '7',
    SUM(IF(M = 8, TotalAmount, 0 )) AS '8',
    SUM(IF(M = 9, TotalAmount, 0 )) AS '9',
    SUM(IF(M = 10, TotalAmount, 0 )) AS '10',
    SUM(IF(M = 11, TotalAmount, 0 )) AS '11',
    SUM(IF(M = 12, TotalAmount, 0 )) AS '12',
    SUM(TotalAmount) AS Total
FROM (
    SELECT
        MONTH(Expense.ExpenseDate) AS M,
        MONTHNAME(Expense.ExpenseDate) AS MN,
        Expense.CategoryId, 
        ExpenseCategory.CategoryName, 
        SUM(Amount) AS TotalAmount
    FROM Expense INNER JOIN ExpenseCategory ON Expense.CategoryId = ExpenseCategory.CategoryId
    GROUP BY Expense.CategoryId, MONTH(Expense.ExpenseDate)
) T 
GROUP BY T.CategoryId 
ORDER BY T.CategoryId ASC;

ผลลัพธ์ก็จะประมาณนี้

  • แถวเป็นประเภทค่าใช้จ่าย
  • คอลัมภ์เป็นยอดรวมรายเดือน

แบบนี้บ่อย ๆ ก็เหนื่อยเหมือนกันนะ  (อาจมีคนเขียน SQL สั้นและง่ายกว่านี้แหล่ะ  ก็ช่วยแนะนำด้วยหล่ะกันครับ) มันต้องมีคนเคยเหนื่อยเหมือนเรานะ และก็มีจริงด้วย คุณ Rick James เค้าก็ทำ Function เพื่อทำ Pivot Table ไว้เหมือนกัน (Blog นี้แนะนำให้อ่านเลย)

DELIMITER //
DROP   PROCEDURE IF EXISTS PIVOTTABLE //
CREATE PROCEDURE PIVOTTABLE(
    IN tbl_name VARCHAR(99),       -- table name (or db.tbl)
    IN base_cols VARCHAR(99),      -- column(s) on the left, separated by commas
    IN pivot_col VARCHAR(64),      -- name of column to put across the top
    IN tally_col VARCHAR(64),      -- name of column to SUM up
    IN where_clause VARCHAR(99),   -- empty string or "WHERE ..."
    IN order_by VARCHAR(99)        -- empty string or "ORDER BY ..."; usually the base_cols
    )
    DETERMINISTIC
    SQL SECURITY INVOKER
BEGIN
    -- Find the distinct values
    -- Build the SUM()s
    SET @subq = CONCAT('SELECT DISTINCT ', pivot_col, ' AS val ',
                    ' FROM ', tbl_name, ' ', where_clause, ' ORDER BY 1');
    -- select @subq;

    SET @cc1 = "CONCAT('SUM(IF(&p = ', &v, ', &t, 0)) AS ', &v)";
    SET @cc2 = REPLACE(@cc1, '&p', pivot_col);
    SET @cc3 = REPLACE(@cc2, '&t', tally_col);
    -- select @cc2, @cc3;
    SET @qval = CONCAT("'\"', val, '\"'");
    -- select @qval;
    SET @cc4 = REPLACE(@cc3, '&v', @qval);
    -- select @cc4;

    SET SESSION group_concat_max_len = 10000;   -- just in case
    SET @stmt = CONCAT(
            'SELECT  GROUP_CONCAT(', @cc4, ' SEPARATOR ",\n")  INTO @sums',
            ' FROM ( ', @subq, ' ) AS top');
     select @stmt;
    PREPARE _sql FROM @stmt;
    EXECUTE _sql;                      -- Intermediate step: build SQL for columns
    DEALLOCATE PREPARE _sql;
    -- Construct the query and perform it
    SET @stmt2 = CONCAT(
            'SELECT ',
                base_cols, ',\n',
                @sums,
                ',\n SUM(', tally_col, ') AS Total'
            '\n FROM ', tbl_name, ' ',
            where_clause,
            ' GROUP BY ', base_cols,
            '\n WITH ROLLUP',
            '\n', order_by
        );
    select @stmt2;                    -- The statement that generates the result
    PREPARE _sql FROM @stmt2;
    EXECUTE _sql;                     -- The resulting pivot table ouput
    DEALLOCATE PREPARE _sql;
    -- For debugging / tweaking, SELECT the various @variables after CALLing.
END;
//
DELIMITER ;

วิธีเรียกใช้งานก็ง่าย ๆ เลยแบบนี้

CALL PIVOTTABLE('TEMP', 'CategoryName', 'M', 'TotalAmount', '', ' ORDER BY CategoryId');

ตาราง TEMP ก็สร้างขึ้นมาจากสรุป (ตารางฝั่งซ้ายจากรูปแรก)

DROP TABLE IF EXISTS TEMP;
CREATE TABLE TEMP (
    SELECT
        MONTH(Expense.ExpenseDate) AS M,
        MONTHNAME(Expense.ExpenseDate) AS MN,
        Expense.CategoryId AS CategoryId, 
        ExpenseCategory.CategoryName AS CategoryName, 
        SUM(Amount) AS TotalAmount
    FROM Expense INNER JOIN ExpenseCategory ON Expense.CategoryId = ExpenseCategory.CategoryId
    GROUP BY Expense.CategoryId, MONTH(Expense.ExpenseDate)
);

SELECT * FROM TEMP;

หลังจากเรียกฟังก์ชั่นเสร็จผลลัพธ์ก็เหมือนที่เราเขียนเองนั่นแหล่ะ ^_^ แต่ลดแรงงานได้อีก

CALL PIVOTTABLE('TEMP', 'CategoryName', 'M', 'TotalAmount', '', ' ORDER BY CategoryId');

ป.ล.

HDC V4.0 & Smart Card On Ubuntu

เนื่องจาก HDC ของกระทรวงสาธารณสุขได้ปรับปรุงเวอร์ชั่นใหม่ ทำให้การเข้าใช้งานระบบด้วย Smart Card ด้วย Linux แบบเดิมไม่สามารถทำได้ เคยเขียนวิธีใช้เวอร์ชั่นเดิมไว้ที่นี่ (HDC & Smart Card On Ubuntu)

ถ้าจากข้อมูลการปรับปรุงจะเห็นว่า

  • มีการใช้ HTTPS
  • มีการปรับปรุง Agent

ที่นี้เราจะเข้าใช้งานระบบด้วย Smart Card ด้วย Linux (ในที่นี้ใช้ Ubuntu 18.04)

  1. ทำการดาวน์โหลด Smart Card Agent เวอร์ชั่นปรับปรุง ทำการติดตั้งให้เรียบร้อย
  2. ตรวจสอบ Smart Card ในเครื่อง ถ้าพบเครื่องอ่าน Smart Card จะขึ้นหน้าตาแบบนี้ (กรณีไม่พบให้ทำการติดตั้งแพคเกจ sudo apt install pcscd pcsc-tools
  3. ทำการรัน Smart Card Agent ได้เลย (ข้ามเรื่องติดตั้ง JRE ไปเลยนะ ถ้าไม่มีก็ติดตั้งซะ)
    java -Dsun.security.smartcardio.library=/lib/x86_64-linux-gnu/libpcsclite.so.1 -jar JSmartCardReader.jar 8084

  4. เปิดหน้า Login ของ HDC ก็เป็นอันสามารถเข้าใช้งานได้ เคลียร์ !!!

ป.ล. ที่เปลี่ยนแปลงจากเวอร์ชั่นเดิม

  • Port ปรับมาใช้ 8084 ในตัว Agent
  • ใช้ HTTPS