Merge branch 'master' of github.com:cmcaine/tridactyl into glacambre-make_scrolling_faster

This commit is contained in:
Oliver Blanthorn 2018-05-30 11:28:46 +01:00
commit ce019627ed
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
3 changed files with 412 additions and 64 deletions

View file

@ -1,25 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
import json
import os
import pathlib
import re
import shutil
import struct
import subprocess
import sys
import tempfile
import time
import unicodedata
VERSION = "0.1.5"
DEBUG = False
VERSION = "0.1.6"
class NoConnectionError(Exception):
""" Exception thrown when stdin cannot be read """
def is_command_on_path(command):
""" Returns 'True' if the if the specified command is found on
user's $PATH.
"""
if shutil.which(command):
return True
else:
return False
def eprint(*args, **kwargs):
""" Print to stderr, which gets echoed in the browser console when run
by Firefox
""" Print to stderr, which gets echoed in the browser console
when run by Firefox
"""
print(*args, file=sys.stderr, flush=True, **kwargs)
@ -41,8 +55,8 @@ def getMessage():
rawLength = sys.stdin.buffer.read(4)
if len(rawLength) == 0:
sys.exit(0)
messageLength = struct.unpack('@I', rawLength)[0]
message = sys.stdin.buffer.read(messageLength).decode('utf-8')
messageLength = struct.unpack("@I", rawLength)[0]
message = sys.stdin.buffer.read(messageLength).decode("utf-8")
return json.loads(message)
@ -50,18 +64,18 @@ def getMessage():
# given its content.
def encodeMessage(messageContent):
""" Encode a message for transmission, given its content."""
encodedContent = json.dumps(messageContent).encode('utf-8')
encodedLength = struct.pack('@I', len(encodedContent))
return {'length': encodedLength, 'content': encodedContent}
encodedContent = json.dumps(messageContent).encode("utf-8")
encodedLength = struct.pack("@I", len(encodedContent))
return {"length": encodedLength, "content": encodedContent}
# Send an encoded message to stdout
def sendMessage(encodedMessage):
""" Send an encoded message to stdout."""
sys.stdout.buffer.write(encodedMessage['length'])
sys.stdout.buffer.write(encodedMessage['content'])
sys.stdout.buffer.write(encodedMessage["length"])
sys.stdout.buffer.write(encodedMessage["content"])
try:
sys.stdout.buffer.write(encodedMessage['code'])
sys.stdout.buffer.write(encodedMessage["code"])
except KeyError:
pass
@ -72,13 +86,15 @@ def findUserConfigFile():
""" Find a user config file, if it exists. Return the file path, or None
if not found
"""
home = os.path.expanduser('~')
config_dir = getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
home = os.path.expanduser("~")
config_dir = getenv(
"XDG_CONFIG_HOME", os.path.expanduser("~/.config")
)
# Will search for files in this order
candidate_files = [
os.path.join(config_dir, "tridactyl", "tridactylrc"),
os.path.join(home, '.tridactylrc')
os.path.join(home, ".tridactylrc"),
]
config_path = None
@ -102,7 +118,7 @@ def getUserConfig():
# for now, this is a simple file read, but if the files can
# include other files, that will need more work
return open(cfg_file, 'r').read()
return open(cfg_file, "r").read()
def sanitizeFilename(fn):
@ -110,30 +126,292 @@ def sanitizeFilename(fn):
From https://stackoverflow.com/a/295466/147356"""
fn = unicodedata.normalize('NFKD', fn).encode(
'ascii', 'ignore').decode('ascii')
fn = re.sub('[^\w\s/.-]', '', fn).strip().lower()
fn = re.sub('\.\.+', '', fn)
fn = re.sub('[-/\s]+', '-', fn)
fn = (
unicodedata.normalize("NFKD", fn)
.encode("ascii", "ignore")
.decode("ascii")
)
fn = re.sub("[^\w\s/.-]", "", fn).strip().lower()
fn = re.sub("\.\.+", "", fn)
fn = re.sub("[-/\s]+", "-", fn)
return fn
def is_valid_firefox_profile(profile_dir):
is_valid = False
validity_indicator = "times.json"
if pathlib.WindowsPath(profile_dir).is_dir():
test_path = "%s\\%s" % (profile_dir, validity_indicator)
if pathlib.WindowsPath(test_path).is_file():
is_valid = True
return is_valid
def win_firefox_restart(message):
"""Handle 'win_firefox_restart' message."""
reply = {}
profile_dir = None
browser_cmd = None
try:
profile_dir = message["profiledir"].strip()
browser_cmd = message["browsercmd"].strip()
except KeyError:
reply = {
"code": -1,
"cmd": "error",
"error": "Error parsing 'restart' message.",
}
return reply
if (
profile_dir
and profile_dir != "auto"
and not is_valid_firefox_profile(profile_dir)
):
reply = {
"code": -1,
"cmd": "error",
"error": "%s %s %s"
% (
"Invalid profile directory specified.",
"Vaild profile directory path(s) can be found by",
"navigating to 'about:support'.",
),
}
elif browser_cmd and not is_command_on_path(browser_cmd):
reply = {
"code": -1,
"cmd": "error",
"error": "%s %s %s"
% (
"'{0}' wasn't found on %PATH%.".format(browser_cmd),
"Please set valid browser by",
"'set browser [browser-command]'.",
),
}
else:
# {{{
# Native messenger can't seem to create detached process on
# Windows while Firefox is quitting, which is essential to
# trigger restarting Firefox. So, below we are resorting to
# create a scheduled task with the task start-time set in
# the near future.
#
#
# subprocess.Popen(
# [ff_bin_path, "-profile", profile_dir],
# shell=False,
# creationflags=0x208 \
# | subprocess.CREATE_NEW_PROCESS_GROUP)
#
#
# 'schtasks.exe' is limited as in it doesn't support
# task-time with granularity in seconds. So, falling back
# to PowerShell as the last resort.
#
# out_str = ""
# task_time = time.strftime("%H:%M",
# time.localtime(
# time.time() + 60))
#
# out_str = subprocess.check_output(
# ["schtasks.exe",
# "/Create",
# "/F",
# "/SC",
# "ONCE",
# "/TN",
# "tridactyl",
# "/TR",
# "calc",
# "/IT",
# "/ST",
# task_time],
# shell=True)
# }}}
ff_lock_name = "parent.lock"
ff_bin_name = browser_cmd
ff_bin_path = '"%s"' % shutil.which(ff_bin_name)
ff_bin_dir = '"%s"' % str(
pathlib.WindowsPath(shutil.which(ff_bin_name)).parent
)
if profile_dir == "auto":
ff_lock_path = ff_bin_path
ff_args = '"%s"' % ("-foreground")
else:
ff_lock_path = '"%s/%s"' % (profile_dir, ff_lock_name)
ff_args = '"%s","%s","%s"' % (
"-foreground",
"-profile",
profile_dir,
)
try:
restart_ps1_content = """
$env:PATH=$env:PATH;{ff_bin_dir}
Set-Location -Path {ff_bin_dir}
$profileDir = "{profile_dir}"
if ($profileDir -ne "auto") {{
$lockFilePath = {ff_lock_path}
$locked = $true
$num_try = 10
}} else {{
$locked = $false
}}
while (($locked -eq $true) -and ($num_try -gt 0)) {{
try {{
[IO.File]::OpenWrite($lockFilePath).close()
$locked=$false
}} catch {{
$num_try-=1
Write-Host "[+] Trial: $num_try [lock == true]"
Start-Sleep -Seconds 1
}}
}}
if ($locked -eq $true) {{
$errorMsg = "Restarting Firefox failed. Please restart manually."
Write-Host "$errorMsg"
# Add-Type -AssemblyName System.Windows.Forms
# [System.Windows.MessageBox]::Show(
# $errorMsg,
# "Tridactyl")
}} else {{
Write-Host "[+] Restarting Firefox ..."
Start-Process `
-WorkingDirectory {ff_bin_dir} `
-FilePath {ff_bin_path} `
-ArgumentList {ff_args} `
-WindowStyle Normal
}}
""".format(
ff_bin_dir=ff_bin_dir,
profile_dir=profile_dir,
ff_lock_path=ff_lock_path,
ff_bin_path=ff_bin_path,
ff_args=ff_args,
)
delay_sec = 1.5
task_name = "firefox-restart"
native_messenger_dirname = ".tridactyl"
powershell_cmd = "powershell"
powershell_args = "%s %s" % (
"-NoProfile",
"-ExecutionPolicy Bypass",
)
restart_ps1_path = "%s\\%s\\%s" % (
os.path.expanduser("~"),
native_messenger_dirname,
"win_firefox_restart.ps1",
)
task_cmd = "cmd"
task_arg = '/c "%s %s -File %s"' % (
powershell_cmd,
powershell_args,
restart_ps1_path,
)
open(restart_ps1_path, "w+").write(restart_ps1_content)
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_output(
[
"powershell",
"-NonInteractive",
"-NoProfile",
"-WindowStyle",
"Minimized",
"-InputFormat",
"None",
"-ExecutionPolicy",
"Bypass",
"-Command",
"Register-ScheduledTask \
-TaskName '%s' \
-Force \
-Action (New-ScheduledTaskAction \
-Execute '%s' \
-Argument '%s') \
-Trigger (New-ScheduledTaskTrigger \
-Once \
-At \
(Get-Date).AddSeconds(%d).ToString('HH:mm:ss'))"
% (task_name, task_cmd, task_arg, delay_sec),
],
shell=False,
startupinfo=startupinfo,
)
reply = {
"code": 0,
"content": "Restarting in %d seconds..."
% delay_sec,
}
except subprocess.CalledProcessError:
reply = {
"code": -1,
"cmd": "error",
"error": "error creating restart task.",
}
return reply
def write_log(msg):
debug_log_dirname = ".tridactyl"
debug_log_filename = "native_main.log"
debug_log_path = "%s\\%s\\%s" % (
os.path.expanduser("~"),
debug_log_dirname,
debug_log_filename,
)
open(debug_log_path, "a+").write(msg)
def handleMessage(message):
""" Generate reply from incoming message. """
cmd = message["cmd"]
reply = {'cmd': cmd}
reply = {"cmd": cmd}
if cmd == 'version':
reply = {'version': VERSION}
if DEBUG:
msg = "%s %s\n" % (
time.strftime("%H:%M:%S %p", time.localtime()),
str(message),
)
write_log(msg)
elif cmd == 'getconfig':
if cmd == "version":
reply = {"version": VERSION}
elif cmd == "getconfig":
file_content = getUserConfig()
if file_content:
reply['content'] = file_content
reply["content"] = file_content
else:
reply['code'] = 'File not found'
reply["code"] = "File not found"
elif cmd == 'run':
elif cmd == "run":
commands = message["command"]
try:
@ -145,47 +423,56 @@ def handleMessage(message):
reply["code"] = process.returncode
reply["content"] = process.output.decode("utf-8")
elif cmd == 'eval':
elif cmd == "eval":
output = eval(message["command"])
reply['content'] = output
reply["content"] = output
elif cmd == 'read':
elif cmd == "read":
try:
with open(os.path.expandvars(os.path.expanduser(message["file"])), "r") as file:
reply['content'] = file.read()
reply['code'] = 0
with open(
os.path.expandvars(
os.path.expanduser(message["file"])
),
"r",
) as file:
reply["content"] = file.read()
reply["code"] = 0
except FileNotFoundError:
reply['content'] = ""
reply['code'] = 2
reply["content"] = ""
reply["code"] = 2
elif cmd == 'mkdir':
elif cmd == "mkdir":
os.makedirs(
os.path.relpath(message["dir"]), exist_ok=message["exist_ok"]
os.path.relpath(message["dir"]),
exist_ok=message["exist_ok"],
)
reply['content'] = ""
reply['code'] = 0
reply["content"] = ""
reply["code"] = 0
elif cmd == 'write':
elif cmd == "write":
with open(message["file"], "w") as file:
file.write(message["content"])
elif cmd == 'temp':
prefix = message.get('prefix')
elif cmd == "temp":
prefix = message.get("prefix")
if prefix is None:
prefix = ''
prefix = 'tmp_{}_'.format(sanitizeFilename(prefix))
prefix = ""
prefix = "tmp_{}_".format(sanitizeFilename(prefix))
(handle, filepath) = tempfile.mkstemp(prefix=prefix)
with os.fdopen(handle, "w") as file:
file.write(message["content"])
reply['content'] = filepath
reply["content"] = filepath
elif cmd == 'env':
reply['content'] = getenv(message["var"], "")
elif cmd == "env":
reply["content"] = getenv(message["var"], "")
elif cmd == "win_firefox_restart":
reply = win_firefox_restart(message)
else:
reply = {'cmd': 'error', 'error': 'Unhandled message'}
eprint('Unhandled message: {}'.format(message))
reply = {"cmd": "error", "error": "Unhandled message"}
eprint("Unhandled message: {}".format(message))
return reply

View file

@ -407,11 +407,24 @@ export async function updatenative(interactive = true) {
*/
//#background
export async function restart() {
const firefox = (await Native.ffargs()).join(" ")
const profile = await Native.getProfileDir()
// Wait for the lock to disappear, then wait a bit more, then start firefox
Native.run(`while readlink ${profile}/lock ; do sleep 1 ; done ; sleep 1 ; ${firefox}`)
qall()
const profiledir = await Native.getProfileDir()
const browsercmd = await config.get("browser")
if ((await browser.runtime.getPlatformInfo()).os === "win") {
let reply = await Native.winFirefoxRestart(profiledir, browsercmd)
logger.info("[+] win_firefox_restart 'reply' = " + JSON.stringify(reply))
if (Number(reply["code"]) === 0) {
fillcmdline("#" + reply["content"])
qall()
} else {
fillcmdline("#" + reply["error"])
}
} else {
const firefox = (await Native.ffargs()).join(" ")
// Wait for the lock to disappear, then wait a bit more, then start firefox
Native.run(`while readlink ${profiledir}/lock ; do sleep 1 ; done ; sleep 1 ; ${firefox}`)
qall()
}
}
// }}}

View file

@ -20,6 +20,7 @@ type MessageCommand =
| "eval"
| "getconfig"
| "env"
| "win_firefox_restart"
interface MessageResp {
cmd: string
version: number | null
@ -245,6 +246,19 @@ export async function temp(content: string, prefix: string) {
return sendNativeMsg("temp", { content, prefix })
}
export async function winFirefoxRestart(
profiledir: string,
browsercmd: string,
) {
let required_version = "0.1.6"
if (!await nativegate(required_version, false)) {
throw `'restart' on Windows needs native messenger version >= ${required_version}.`
}
return sendNativeMsg("win_firefox_restart", { profiledir, browsercmd })
}
export async function run(command: string) {
let msg = await sendNativeMsg("run", { command })
logger.info(msg)
@ -259,10 +273,12 @@ export async function pyeval(command: string): Promise<MessageResp> {
}
export async function getenv(variable: string) {
let v = await getNativeMessengerVersion()
if (!await nativegate("0.1.2", false)) {
throw `Error: getenv needs native messenger v>=0.1.2. Current: ${v}`
let required_version = "0.1.2"
if (!await nativegate(required_version, false)) {
throw `'getenv' needs native messenger version >= ${required_version}.`
}
return (await sendNativeMsg("env", { var: variable })).content
}
@ -270,14 +286,46 @@ export async function getenv(variable: string) {
You'll get both firefox binary (not necessarily an absolute path) and flags */
export async function ffargs(): Promise<string[]> {
// Using ' and + rather that ` because we don't want newlines
let output = await pyeval(
'handleMessage({"cmd": "run", ' +
'"command": "ps -p " + str(os.getppid()) + " -oargs="})["content"]',
)
return output.content.trim().split(" ")
if ((await browserBg.runtime.getPlatformInfo()).os === "win") {
throw `Error: "ffargs() is currently broken on Windows and should be avoided."`
} else {
let output = await pyeval(
'handleMessage({"cmd": "run", ' +
'"command": "ps -p " + str(os.getppid()) + " -oargs="})["content"]',
)
return output.content.trim().split(" ")
}
}
export async function getProfileDir() {
// Windows users must explicitly set their Firefox profile
// directory via 'set profiledir [directory]', or use the
// default 'profiledir' value as 'auto' (without quotes).
//
// Profile directory paths on Windows must _not_ be escaped, and
// should be used exactly as shown in the 'about:support' page.
//
// Example:
//
// :set profiledir C:\Users\<User-Name>\AppData\Roaming\Mozilla\Firefox\Profiles\8s21wzbh.Default
//
if ((await browserBg.runtime.getPlatformInfo()).os === "win") {
let win_profiledir = config.get("profiledir")
win_profiledir = win_profiledir.trim()
logger.info("[+] profiledir original: " + win_profiledir)
win_profiledir = win_profiledir.replace(/\\/g, "/")
logger.info("[+] profiledir escaped: " + win_profiledir)
if (win_profiledir.length > 0) {
return win_profiledir
} else {
throw new Error(
"Profile directory is not set. Profile directory path(s) can be found by navigating to 'about:support'.",
)
}
}
// First, see if we can get the profile from the arguments that were given
// to Firefox
let args = await ffargs()