2015-10-15 15:25:10 +00:00
|
|
|
xvfb = require("xvfb")
|
|
|
|
log = require("../logger")("TS3Client")
|
|
|
|
config = require("../config")
|
|
|
|
services = require("../services")
|
|
|
|
x11tools = require("../x11")
|
|
|
|
TS3Settings = require("../ts3settings")
|
|
|
|
TS3ClientQuery = require("../ts3query")
|
|
|
|
path = require "path"
|
|
|
|
merge = require "merge"
|
2015-10-16 06:22:20 +00:00
|
|
|
fs = require "fs"
|
2015-10-15 15:25:10 +00:00
|
|
|
spawn = require("child_process").spawn
|
|
|
|
StreamSplitter = require("stream-splitter")
|
|
|
|
require_bin = require("../require_bin")
|
|
|
|
|
|
|
|
ts3client_binpath = require_bin path.join(config.get("ts3-install-path"), "ts3client_linux_" + (if process.arch == "x64" then "amd64" else process.arch))
|
|
|
|
|
|
|
|
module.exports = class TS3ClientService extends services.Service
|
|
|
|
dependencies: [
|
|
|
|
"xvfb",
|
|
|
|
"blackbox",
|
|
|
|
"pulseaudio"
|
|
|
|
]
|
|
|
|
constructor: -> super "TS3Client",
|
|
|
|
start: (args, cb) =>
|
|
|
|
if typeof args == "function"
|
|
|
|
cb = args
|
|
|
|
args = null
|
|
|
|
|
|
|
|
if @process
|
|
|
|
cb? null, @process
|
|
|
|
return
|
|
|
|
|
|
|
|
if not args
|
|
|
|
args = []
|
|
|
|
|
2015-10-16 07:36:52 +00:00
|
|
|
await fs.access ts3client_binpath, fs.R_OK | fs.X_OK, defer err
|
|
|
|
if err
|
2015-10-16 07:47:58 +00:00
|
|
|
log.error "Can't access TeamSpeak3 client binary at #{ts3client_binpath}, does the binary exist and have you given correct access?"
|
2015-10-16 07:36:52 +00:00
|
|
|
cb? new Error "Access to TeamSpeak3 binary failed."
|
|
|
|
return
|
|
|
|
|
2015-10-15 15:25:10 +00:00
|
|
|
await @_preconfigure defer()
|
|
|
|
|
|
|
|
# spawn process
|
|
|
|
proc = null
|
|
|
|
doStart = null
|
|
|
|
forwardLog = (token) =>
|
|
|
|
token = token.trim() # get rid of \r
|
|
|
|
if token.indexOf("|") > 0
|
|
|
|
token = token.split("|")
|
|
|
|
level = token[1].toUpperCase().trim()
|
|
|
|
source = token[2].trim()
|
|
|
|
sourceStr = if source then "#{source}: " else ""
|
|
|
|
message = token[4].trim()
|
|
|
|
switch level
|
|
|
|
when "ERROR" then log.error "%s%s", sourceStr, message
|
|
|
|
when "WARN" then log.warn "%s%s", sourceStr, message
|
|
|
|
when "INFO" then log.debug "%s%s", sourceStr, message
|
|
|
|
else log.silly "%s%s", sourceStr, message
|
|
|
|
else
|
|
|
|
log.debug token
|
|
|
|
scheduleRestart = null # see below
|
|
|
|
onExit = => # autorestart
|
|
|
|
@_running = false
|
|
|
|
@process = null
|
|
|
|
if @_requestedExit
|
|
|
|
return
|
|
|
|
log.warn "TeamSpeak3 unexpectedly terminated!"
|
|
|
|
scheduleRestart()
|
|
|
|
doStart = () =>
|
|
|
|
env =
|
|
|
|
HOME: process.env.HOME
|
|
|
|
DISPLAY: process.env.DISPLAY
|
|
|
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR
|
|
|
|
KDEDIRS: ''
|
|
|
|
KDEDIR: ''
|
|
|
|
QTDIR: config.get("ts3-install-path")
|
|
|
|
QT_PLUGIN_PATH: config.get("ts3-install-path")
|
|
|
|
LD_LIBRARY_PATH: config.get("ts3-install-path")
|
|
|
|
if process.env.LD_LIBRARY_PATH
|
|
|
|
env.LD_LIBRARY_PATH += ":#{process.env.LD_LIBRARY_PATH}"
|
|
|
|
|
|
|
|
@log.silly "Environment variables:", env
|
|
|
|
@log.silly "Arguments:", JSON.stringify args
|
|
|
|
|
|
|
|
@_requestedExit = false
|
|
|
|
proc = spawn ts3client_binpath, args,
|
|
|
|
detached: true
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
|
|
cwd: config.get("ts3-install-path")
|
|
|
|
env: env
|
|
|
|
@_running = true
|
|
|
|
|
|
|
|
# logging
|
|
|
|
stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n"
|
|
|
|
stdoutTokenizer.encoding = "utf8";
|
|
|
|
stdoutTokenizer.on "token", forwardLog
|
|
|
|
|
|
|
|
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
|
|
|
|
stderrTokenizer.encoding = "utf8";
|
|
|
|
stderrTokenizer.on "token", forwardLog
|
|
|
|
|
|
|
|
# connect to client query plugin when it's loaded
|
|
|
|
stdoutTokenizer.on "token", (token) =>
|
|
|
|
if token.indexOf("Loading plugin: libclientquery_plugin") >= 0
|
|
|
|
# client query plugin is now loading
|
|
|
|
@_queryReconnectTimer = setTimeout @query.connect.bind(@query), 250
|
|
|
|
stderrTokenizer.on "token", (token) =>
|
|
|
|
if token.indexOf("Query: bind failed") >= 0
|
|
|
|
# without query this ts3 instance is worthless
|
|
|
|
await @_ts3GracefulShutdown defer()
|
|
|
|
scheduleRestart()
|
|
|
|
|
|
|
|
# autorestart
|
|
|
|
proc.on "exit", onExit
|
|
|
|
|
|
|
|
@process = proc
|
|
|
|
scheduleRestart = () =>
|
|
|
|
log.warn "Restarting in 5 seconds..."
|
|
|
|
@_startTimer = setTimeout doStart.bind(@), 5000
|
|
|
|
if @_queryReconnectTimer
|
|
|
|
clearTimeout @_queryReconnectTimer
|
|
|
|
doStart()
|
|
|
|
|
|
|
|
# ts3 query
|
|
|
|
@query = new TS3ClientQuery "127.0.0.1", 25639
|
|
|
|
@_queryReconnectTimer = null
|
|
|
|
@query.on "error", (err) =>
|
2015-10-16 08:25:19 +00:00
|
|
|
log.warn "Error in TS3 query connection", err
|
|
|
|
@query.on "close", =>
|
|
|
|
if not @_requestedExit
|
|
|
|
log.warn "Connection to TS3 client query interface lost, reconnecting..."
|
2015-10-15 15:25:10 +00:00
|
|
|
@_queryReconnectTimer = setTimeout @query.connect.bind(@query), 1000
|
|
|
|
else
|
2015-10-16 08:25:19 +00:00
|
|
|
log.debug "Connection to TS3 client query interface lost."
|
2015-10-15 15:25:10 +00:00
|
|
|
@query.on "open", => log.debug "Connected to TS3 client query interface."
|
|
|
|
@query.on "connecting", => log.debug "Connecting to TS3 client query interface..."
|
|
|
|
|
|
|
|
cb? null, @process
|
|
|
|
|
|
|
|
stop: (cb) -> @_ts3GracefulShutdown cb
|
|
|
|
|
|
|
|
_ts3GracefulShutdown: (cb) ->
|
|
|
|
@_requestedExit = true
|
|
|
|
|
|
|
|
if @_startTimer
|
|
|
|
clearTimeout @_startTimer
|
|
|
|
|
|
|
|
if @_queryReconnectTimer
|
|
|
|
clearTimeout @_queryReconnectTimer
|
|
|
|
|
|
|
|
if @_running
|
|
|
|
log.silly "Using xdotool to gracefully shut down TS3"
|
|
|
|
await x11tools.getWindowIdByProcessId @process.pid, defer(err, wid)
|
|
|
|
if not wid
|
|
|
|
log.debug "Can not find a window for #{@name}."
|
|
|
|
log.warn "Can not properly shut down #{@name}, it will time out on the server instead."
|
|
|
|
@process.kill()
|
|
|
|
else
|
|
|
|
log.silly "Sending keys to TS3"
|
|
|
|
await x11tools.sendKeys wid, "ctrl+q", defer(err)
|
|
|
|
if err
|
|
|
|
log.warn "Can not properly shut down #{@name}, it will time out on the server instead."
|
|
|
|
log.silly "Using SIGTERM for shutdown of #{@name}"
|
|
|
|
@process.kill()
|
|
|
|
|
|
|
|
# wait for 10 seconds then SIGKILL if still up
|
|
|
|
log.silly "Now waiting 10 seconds for shutdown..."
|
|
|
|
killTimer = setTimeout (() =>
|
|
|
|
log.silly "10 seconds gone, using SIGKILL now since we're impatient."
|
|
|
|
@process.kill("SIGKILL")), 10000
|
|
|
|
await @process.once "exit", defer()
|
|
|
|
clearTimeout killTimer
|
|
|
|
|
|
|
|
@_running = false
|
|
|
|
@process = null
|
|
|
|
else
|
|
|
|
log.warn "TeamSpeak3 seems to have died prematurely."
|
|
|
|
|
|
|
|
cb?()
|
|
|
|
|
|
|
|
_preconfigure: (cb) =>
|
|
|
|
ts3settings = new TS3Settings config.get("ts3-config-path")
|
|
|
|
await ts3settings.open defer()
|
|
|
|
|
|
|
|
# Delete bookmars to prevent auto-connect bookmarks from weirding out the client
|
|
|
|
await ts3settings.query "delete * from Bookmarks", defer()
|
|
|
|
|
|
|
|
# Let's make sure we have an identity!
|
|
|
|
force = ts3settings.getIdentitiesSize() <= 0 or config.get("identity-path")
|
|
|
|
if force
|
|
|
|
if not config.get("identity-path")
|
|
|
|
throw new Error "Need a file to import the bot's identity from."
|
|
|
|
ts3settings.clearIdentities()
|
|
|
|
await ts3settings.importIdentity config.get("identity-path"), defer(identity)
|
|
|
|
identity.select()
|
|
|
|
|
|
|
|
if config.get("nickname")
|
|
|
|
# Enforce nickname from configuration
|
|
|
|
identity = ts3settings.getSelectedIdentity()
|
|
|
|
identity.nickname = config.get "nickname"
|
|
|
|
identity.update()
|
|
|
|
|
|
|
|
# Some settings to help the TS3Bot to do what it's supposed to do
|
2015-10-16 08:11:38 +00:00
|
|
|
now = new Date()
|
2015-10-15 15:25:10 +00:00
|
|
|
await ts3settings.setMultiple [
|
|
|
|
[ "Application", "HotkeyMode", "2" ]
|
|
|
|
[ "Chat", "MaxLines", "1" ]
|
|
|
|
[ "Chat", "LogChannelChats", "0" ]
|
|
|
|
[ "Chat", "LogClientChats", "0" ]
|
|
|
|
[ "Chat", "LogServerChats", "0" ]
|
|
|
|
[ "Chat", "ReloadChannelChats", "0" ]
|
|
|
|
[ "Chat", "ReloadClientChats", "0" ]
|
|
|
|
[ "Chat", "IndicateChannelChats", "0" ]
|
|
|
|
[ "Chat", "IndicatePrivateChats", "0" ]
|
|
|
|
[ "Chat", "IndicateServerChats", "0" ]
|
2015-10-16 06:23:37 +00:00
|
|
|
[ "ClientLogView", "LogLevel", "000001" ]
|
2015-10-15 15:25:10 +00:00
|
|
|
[ "FileTransfer", "SimultaneousDownloads", "2" ]
|
|
|
|
[ "FileTransfer", "SimultaneousUploads", "2" ]
|
|
|
|
[ "FileTransfer", "UploadBandwidth", "0" ]
|
|
|
|
[ "FileTransfer", "DownloadBandwidth", "0" ]
|
|
|
|
[ "General", "LastShownLicense", "1" ] # ugh...
|
|
|
|
[ "General", "LastShownLicenseLang", "C" ]
|
|
|
|
[ "Global", "MainWindowMaximized", "1" ]
|
|
|
|
[ "Global", "MainWindowMaximizedScreen", "1" ]
|
|
|
|
[ "Messages", "Disconnect", config.get "quit-message" ]
|
|
|
|
[ "Misc", "WarnWhenMutedInfoShown", "1" ]
|
|
|
|
[ "Misc", "LastShownNewsBrowserVersion", "4" ]
|
|
|
|
[ "News", "NewsClosed", "1" ]
|
|
|
|
[ "News", "Language", "en" ]
|
2015-10-16 08:11:38 +00:00
|
|
|
[ "News", "LastModified", now.toISOString() ]
|
|
|
|
[ "News", "NextCheck", new Date(now.getTime() + 1000 * 60 * 60 * 24 * 365).toISOString() ]
|
2015-10-16 06:23:15 +00:00
|
|
|
[ "Notifications", "SoundPack", "nosounds" ]
|
2015-10-15 15:25:10 +00:00
|
|
|
[ "Plugins", "teamspeak_control_plugin", "false" ]
|
2015-10-16 08:37:19 +00:00
|
|
|
[ "Plugins", "clientquery_plugin", "true" ]
|
2015-10-15 15:25:10 +00:00
|
|
|
[ "Plugins", "lua_plugin", "false" ]
|
|
|
|
[ "Plugins", "test_plugin", "false" ]
|
|
|
|
[ "Plugins", "ts3g15", "false" ]
|
|
|
|
[ "Profiles", "DefaultPlaybackProfile", "Default" ]
|
|
|
|
[ "Profiles", "Playback/Default", {
|
|
|
|
Device: ''
|
|
|
|
DeviceDisplayName: "Default"
|
|
|
|
VolumeModifier: -40
|
|
|
|
VolumeFactorWaveDb: -17
|
|
|
|
PlayMicClicksOnOwn: false
|
|
|
|
PlayMicClicksOnOthers: false
|
|
|
|
MonoSoundExpansion: 2
|
|
|
|
Mode: "PulseAudio"
|
|
|
|
PlaybackMonoOverCenterSpeaker: false
|
|
|
|
} ]
|
|
|
|
[ "Profiles", "DefaultCaptureProfile", "Default" ]
|
|
|
|
[ "Profiles", "Capture/Default", {
|
|
|
|
Device: ''
|
|
|
|
DeviceDisplayName: "Default"
|
|
|
|
Mode: "PulseAudio"
|
|
|
|
} ]
|
|
|
|
[ "Profiles", "Capture/Default/PreProcessing", {
|
|
|
|
continous_transmission: "false"
|
|
|
|
vad: "true"
|
|
|
|
vad_over_ptt: "false"
|
|
|
|
delay_ptt_msecs: "250"
|
|
|
|
voiceactivation_level: "-49"
|
|
|
|
echo_reduction: false
|
|
|
|
echo_cancellation: false
|
|
|
|
denoise: false
|
|
|
|
delay_ptt: false
|
|
|
|
agc: if config.get("ts3-agc") then "true" else "false"
|
|
|
|
echo_reduction_db: 10
|
|
|
|
} ]
|
|
|
|
[ "Statistics", "ParticipateStatistics", "0" ]
|
|
|
|
[ "Statistics", "ConfirmedParticipation", "1" ]
|
|
|
|
], defer()
|
|
|
|
|
|
|
|
await ts3settings.close defer()
|
|
|
|
|
|
|
|
cb?()
|