summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShaun Taheri <shaun@advancedtelematic.com>2016-10-19 16:15:31 +0200
committerShaun Taheri <shaun@advancedtelematic.com>2016-10-19 16:35:56 +0200
commit41db1050631cfe0aaca8922ec4a58a0f2109ac5d (patch)
tree1c9497eb1c4cb09b8f0125d9fbb1852340fa5213
parente50ec3ba87a744e3b581311efede49e21e9cb018 (diff)
downloadrvi_sota_client-41db1050631cfe0aaca8922ec4a58a0f2109ac5d.tar.gz
Fix generated SOTA.toml files
-rw-r--r--README.md3
-rwxr-xr-xrun/run.sh30
-rw-r--r--run/sota.toml.env6
-rw-r--r--src/datatype/config.rs14
-rw-r--r--src/datatype/system_info.rs21
-rw-r--r--src/gateway/dbus.rs23
-rw-r--r--src/gateway/websocket.rs38
-rw-r--r--src/interpreter.rs20
-rw-r--r--src/main.rs30
-rw-r--r--src/sota.rs3
-rw-r--r--tests/sota.toml6
11 files changed, 95 insertions, 99 deletions
diff --git a/README.md b/README.md
index 31cb67c..673e199 100644
--- a/README.md
+++ b/README.md
@@ -94,11 +94,10 @@ See `tests/genivi.sota.toml` for a sample config. See full documentation for det
Now you can run the `sota_client`:
make client
- ./run/sota_client --config tests/genivi.sota.toml
+ RUST_LOG=debug ./run/sota_client --config tests/genivi.sota.toml
### GENIVI Software Loading Manager
See [genivi_swm](https://github.com/GENIVI/genivi_swm) on how to run the Software Loading Manager demo. It also contains instructions for creating an update image, which can be uploaded as a package to the SOTA Server.
Now you can create an update campaign on the SOTA Server, using the same update_id as the uuid in the update image you created. Also, as the genivi_swm demo runs as root, remember to run the `sota_client` as root as well so that they can communicate on the same system bus.
-
diff --git a/run/run.sh b/run/run.sh
index 4ac4d05..3050f12 100755
--- a/run/run.sh
+++ b/run/run.sh
@@ -25,25 +25,27 @@ if [[ -z "${DEVICE_UUID}" ]]; then
fi
export DEVICE_UUID
-# create or use existing device credentials
-if [[ -z "${AUTH_CLIENT_ID}" ]]; then
- CREDENTIALS=$(http post "${AUTH_SERVER}/clients" \
- client_name="${DEVICE_VIN}" \
- grant_types:='["client_credentials"]' \
- --check-status --print=b)
- AUTH_CLIENT_ID=$(echo "${CREDENTIALS}" | jq -r .client_id)
- AUTH_CLIENT_SECRET=$(echo "${CREDENTIALS}" | jq -r .client_secret)
-fi
-export AUTH_CLIENT_ID
-export AUTH_CLIENT_SECRET
+[[ "${AUTH_SECTION}" = true ]] && {
+ # create or use existing device credentials
+ if [[ -z "${AUTH_CLIENT_ID}" ]]; then
+ CREDENTIALS=$(http post "${AUTH_SERVER}/clients" \
+ client_name="${DEVICE_VIN}" \
+ grant_types:='["client_credentials"]' \
+ --check-status --print=b)
+ AUTH_CLIENT_ID=$(echo "${CREDENTIALS}" | jq -r .client_id)
+ AUTH_CLIENT_SECRET=$(echo "${CREDENTIALS}" | jq -r .client_secret)
+ fi
+ export AUTH_CLIENT_ID
+ export AUTH_CLIENT_SECRET
+} || {
+ # remove [auth] section
+ sed -i '/\[core\]/,$!d' "${TEMPLATE_PATH}"
+}
# generate sota.toml config
echo "---START CONFIG---"
envsubst < "${TEMPLATE_PATH}" | tee "${OUTPUT_PATH}"
echo "---END CONFIG---"
-
-# optionally remove auth section and/or quit
-[[ "${AUTH_SECTION}" = false ]] && sed -i '/\[core\]/,$!d' "${OUTPUT_PATH}"
[[ "${CONFIG_ONLY}" = true ]] && exit 0
# set up dbus
diff --git a/run/sota.toml.env b/run/sota.toml.env
index ecf15b5..cd7d460 100644
--- a/run/sota.toml.env
+++ b/run/sota.toml.env
@@ -14,7 +14,7 @@ DEVICE_PACKAGES_DIR=/tmp/
DEVICE_PACKAGE_MANAGER=deb
DEVICE_POLLING_INTERVAL=10
DEVICE_CERTIFICATES_PATH=/etc/sota_certificates
-DEVICE_SYSTEM_INFO=system_info.sh
+DEVICE_SYSTEM_INFO=./system_info.sh
GATEWAY_CONSOLE=false
GATEWAY_DBUS=false
@@ -23,8 +23,8 @@ GATEWAY_RVI=false
GATEWAY_SOCKET=false
GATEWAY_WEBSOCKET=true
-NETWORK_HTTP_SERVER=127.0.0.1:8888
-NETWORK_RVI_EDGE_SERVER=127.0.0.1:9080
+NETWORK_HTTP_SERVER=http://127.0.0.1:8888
+NETWORK_RVI_EDGE_SERVER=http://127.0.0.1:9080
NETWORK_SOCKET_COMMANDS_PATH=/tmp/sota-commands.socket
NETWORK_SOCKET_EVENTS_PATH=/tmp/sota-events.socket
NETWORK_WEBSOCKET_SERVER=127.0.0.1:3012
diff --git a/src/datatype/config.rs b/src/datatype/config.rs
index c9c7708..c9905f5 100644
--- a/src/datatype/config.rs
+++ b/src/datatype/config.rs
@@ -204,7 +204,7 @@ pub struct DeviceConfig {
pub vin: String,
pub packages_dir: String,
pub package_manager: PackageManager,
- pub system_info: SystemInfo,
+ pub system_info: Option<SystemInfo>,
pub polling_interval: u64,
pub certificates_path: String,
}
@@ -216,7 +216,7 @@ impl Default for DeviceConfig {
vin: "V1234567890123456".to_string(),
packages_dir: "/tmp/".to_string(),
package_manager: PackageManager::Deb,
- system_info: SystemInfo::default(),
+ system_info: Some(SystemInfo::default()),
polling_interval: 10,
certificates_path: "/tmp/sota_certificates".to_string()
}
@@ -262,8 +262,8 @@ pub struct NetworkConfig {
impl Default for NetworkConfig {
fn default() -> NetworkConfig {
NetworkConfig {
- http_server: "127.0.0.1:8888".to_string(),
- rvi_edge_server: "127.0.0.1:9080".to_string(),
+ http_server: "http://127.0.0.1:8888".to_string(),
+ rvi_edge_server: "http://127.0.0.1:9080".to_string(),
socket_commands_path: "/tmp/sota-commands.socket".to_string(),
socket_events_path: "/tmp/sota-events.socket".to_string(),
websocket_server: "127.0.0.1:3012".to_string()
@@ -327,7 +327,7 @@ mod tests {
[device]
uuid = "123e4567-e89b-12d3-a456-426655440000"
vin = "V1234567890123456"
- system_info = "system_info.sh"
+ system_info = "./system_info.sh"
polling_interval = 10
packages_dir = "/tmp/"
package_manager = "deb"
@@ -348,8 +348,8 @@ mod tests {
const NETWORK_CONFIG: &'static str =
r#"
[network]
- http_server = "127.0.0.1:8888"
- rvi_edge_server = "127.0.0.1:9080"
+ http_server = "http://127.0.0.1:8888"
+ rvi_edge_server = "http://127.0.0.1:9080"
socket_commands_path = "/tmp/sota-commands.socket"
socket_events_path = "/tmp/sota-events.socket"
websocket_server = "127.0.0.1:3012"
diff --git a/src/datatype/system_info.rs b/src/datatype/system_info.rs
index 2d8fff2..987da3b 100644
--- a/src/datatype/system_info.rs
+++ b/src/datatype/system_info.rs
@@ -1,6 +1,5 @@
use rustc_serialize::{Decoder, Decodable};
use std::process::Command;
-use std::str::FromStr;
use datatype::Error;
@@ -13,8 +12,12 @@ pub struct SystemInfo {
impl SystemInfo {
/// Instantiate a new type to report on the system information.
- pub fn new(command: String) -> SystemInfo {
- SystemInfo { command: command }
+ pub fn new(command: &str) -> Option<SystemInfo> {
+ if command == "" {
+ None
+ } else {
+ Some(SystemInfo { command: command.to_string() })
+ }
}
/// Generate a new report of the system information.
@@ -27,20 +30,12 @@ impl SystemInfo {
impl Default for SystemInfo {
fn default() -> SystemInfo {
- SystemInfo::new("system_info.sh".to_string())
- }
-}
-
-impl FromStr for SystemInfo {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<SystemInfo, Error> {
- Ok(SystemInfo::new(s.to_string()))
+ SystemInfo::new("./system_info.sh").expect("couldn't build command")
}
}
impl Decodable for SystemInfo {
fn decode<D: Decoder>(d: &mut D) -> Result<SystemInfo, D::Error> {
- d.read_str().and_then(|s| Ok(s.parse::<SystemInfo>().unwrap()))
+ d.read_str().and_then(|s| SystemInfo::new(&s).ok_or(d.error("bad SystemInfo command path")))
}
}
diff --git a/src/gateway/dbus.rs b/src/gateway/dbus.rs
index 07a3b9c..321e262 100644
--- a/src/gateway/dbus.rs
+++ b/src/gateway/dbus.rs
@@ -24,7 +24,7 @@ impl Gateway for DBus {
thread::spawn(move || {
let conn = Connection::get_private(BusType::Session).expect("couldn't get dbus session");
- conn.register_name(&dbus_cfg.name, NameFlag::ReplaceExisting as u32).unwrap();
+ conn.register_name(&dbus_cfg.name, NameFlag::ReplaceExisting as u32).expect("couldn't register name");
let mut obj_path = ObjectPath::new(&conn, &dbus_cfg.path, true);
obj_path.insert_interface(&dbus_cfg.interface, default_interface(itx));
@@ -33,10 +33,11 @@ impl Gateway for DBus {
loop {
for item in conn.iter(1000) {
if let ConnectionItem::MethodCall(mut msg) = item {
- info!("DBus method call: {:?}", msg);
- obj_path.handle_message(&mut msg).map(|result| {
- let _ = result.map_err(|_| error!("dbus method call failed: {:?}", msg));
- });
+ match obj_path.handle_message(&mut msg) {
+ Some(Ok(())) => info!("DBus message sent: {:?}", msg),
+ Some(Err(())) => error!("DBus message send failed: {:?}", msg),
+ None => debug!("unhandled dbus message: {:?}", msg)
+ }
}
}
}
@@ -48,7 +49,7 @@ impl Gateway for DBus {
fn pulse(&self, event: Event) {
match event {
Event::UpdateAvailable(avail) => {
- let msg = self.new_message("updateAvailable", &[
+ let msg = self.new_swm_message("updateAvailable", &[
MessageItem::from(avail.update_id),
MessageItem::from(avail.signature),
MessageItem::from(avail.description),
@@ -59,7 +60,7 @@ impl Gateway for DBus {
}
Event::DownloadComplete(comp) => {
- let msg = self.new_message("downloadComplete", &[
+ let msg = self.new_swm_message("downloadComplete", &[
MessageItem::from(comp.update_image),
MessageItem::from(comp.signature)
]);
@@ -68,7 +69,7 @@ impl Gateway for DBus {
}
Event::InstalledSoftwareNeeded => {
- let msg = self.new_message("getInstalledPackages", &[
+ let msg = self.new_swm_message("getInstalledPackages", &[
MessageItem::from(true), // include packages?
MessageItem::from(false) // include firmware?
]);
@@ -103,7 +104,7 @@ impl Gateway for DBus {
}
impl DBus {
- fn new_message(&self, method: &str, args: &[MessageItem]) -> Message {
+ fn new_swm_message(&self, method: &str, args: &[MessageItem]) -> Message {
let mgr = self.dbus_cfg.software_manager.clone();
let path = self.dbus_cfg.software_manager_path.clone();
let result = Message::new_method_call(&mgr, &path, &mgr, method);
@@ -139,7 +140,7 @@ fn send(itx: &Sender<Interpret>, cmd: Command) {
fn handle_initiate_download(itx: &Sender<Interpret>, msg: &mut Message) -> MethodResult {
let sender = try!(msg.sender().map(|s| s.to_string()).ok_or(dbus::missing_arg()));
- debug!("handle_initiate_download: sender={:?}, msg={:?}", sender, msg);
+ debug!("dbus handle_initiate_download: sender={:?}, msg={:?}", sender, msg);
let mut args = msg.get_items().into_iter();
let arg_id = try!(args.next().ok_or(dbus::missing_arg()));
@@ -151,7 +152,7 @@ fn handle_initiate_download(itx: &Sender<Interpret>, msg: &mut Message) -> Metho
fn handle_update_report(itx: &Sender<Interpret>, msg: &mut Message) -> MethodResult {
let sender = try!(msg.sender().map(|s| s.to_string()).ok_or(dbus::missing_arg()));
- debug!("handle_update_report: sender ={:?}, msg ={:?}", sender, msg);
+ debug!("dbus handle_update_report: sender={:?}, msg={:?}", sender, msg);
let mut args = msg.get_items().into_iter();
let id_arg = try!(args.next().ok_or(dbus::missing_arg()));
diff --git a/src/gateway/websocket.rs b/src/gateway/websocket.rs
index 72e0889..f63a763 100644
--- a/src/gateway/websocket.rs
+++ b/src/gateway/websocket.rs
@@ -1,12 +1,10 @@
use chan;
use chan::Sender;
use rustc_serialize::json;
-use std::thread;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
-use std::time::Duration;
use ws;
-use ws::{listen, CloseCode, Handler, Handshake, Message, Sender as WsSender};
+use ws::{CloseCode, Handler, Handshake, Message, Sender as WsSender};
use ws::util::Token;
use datatype::{Command, Error, Event};
@@ -22,22 +20,15 @@ pub struct Websocket {
impl Gateway for Websocket {
fn initialize(&mut self, itx: Sender<Interpret>) -> Result<(), String> {
- let clients = self.clients.clone();
- let addr = self.server.clone();
- info!("Opening websocket listener at {}", addr);
-
- thread::spawn(move || {
- listen(&addr as &str, |out| {
- WebsocketHandler {
- out: out,
- itx: itx.clone(),
- clients: clients.clone()
- }
- }).expect("couldn't start websocket listener");
- });
+ ws::listen(&self.server.clone() as &str, |out| {
+ WebsocketHandler {
+ out: out,
+ itx: itx.clone(),
+ clients: self.clients.clone()
+ }
+ }).expect("couldn't start websocket listener");
- thread::sleep(Duration::from_secs(1)); // FIXME: ugly hack for blocking listen call
- Ok(info!("Websocket gateway started."))
+ Ok(info!("Websocket gateway started at {}.", self.server))
}
fn pulse(&self, event: Event) {
@@ -69,7 +60,7 @@ impl Handler for WebsocketHandler {
Err(err)
}
- Err(_) => unreachable!()
+ Err(err) => panic!("unexpected websocket on_message error: {}", err)
})
}
@@ -117,7 +108,7 @@ mod tests {
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use ws;
- use ws::{connect, CloseCode};
+ use ws::CloseCode;
use datatype::{Command, Event};
use gateway::{Gateway, Interpret};
@@ -125,6 +116,7 @@ mod tests {
#[test]
+ #[ignore] // FIXME: wait for https://github.com/housleyjk/ws-rs/issues/64
fn websocket_connections() {
let (etx, erx) = chan::sync::<Event>(0);
let (itx, irx) = chan::sync::<Interpret>(0);
@@ -152,9 +144,9 @@ mod tests {
crossbeam::scope(|scope| {
for id in 0..10 {
scope.spawn(move || {
- connect("ws://localhost:3012", |out| {
- out.send(format!(r#"{{ "variant": "StartDownload", "fields": [["{}"]] }}"#, id))
- .expect("couldn't write to websocket");
+ ws::connect("ws://localhost:3012", |out| {
+ let msg = format!(r#"{{ "variant": "StartDownload", "fields": [["{}"]] }}"#, id);
+ out.send(msg).expect("couldn't write to websocket");
move |msg: ws::Message| {
let ev: Event = json::decode(&format!("{}", msg)).unwrap();
diff --git a/src/interpreter.rs b/src/interpreter.rs
index 7f401de..b15c37e 100644
--- a/src/interpreter.rs
+++ b/src/interpreter.rs
@@ -28,11 +28,11 @@ pub trait Interpreter<I, O> {
let started = time::precise_time_ns();
wg.add(1);
- debug!("interpreter starting: {}", started);
+ trace!("interpreter starting: {}", started);
self.interpret(input, &otx);
thread::sleep(cooldown); // let any further work commence
- debug!("interpreter stopping: {}", started);
+ trace!("interpreter stopping: {}", started);
wg.done();
}
}
@@ -42,7 +42,8 @@ pub trait Interpreter<I, O> {
/// The `EventInterpreter` listens for `Event`s and optionally responds with
/// `Command`s that may be sent to the `CommandInterpreter`.
pub struct EventInterpreter {
- pub pacman: PackageManager
+ pub pacman: PackageManager,
+ pub send_sysinfo: bool,
}
impl Interpreter<Event, Command> for EventInterpreter {
@@ -56,7 +57,10 @@ impl Interpreter<Event, Command> for EventInterpreter {
ctx.send(Command::SendInstalledPackages(packages));
}).unwrap_or_else(|err| error!("couldn't send a list of packages: {}", err));
}
- ctx.send(Command::SendSystemInfo);
+
+ if self.send_sysinfo {
+ ctx.send(Command::SendSystemInfo);
+ }
}
Event::NotAuthenticated => {
@@ -201,8 +205,8 @@ impl<'t> GlobalInterpreter<'t> {
}
Command::ListSystemInfo => {
- let info = try!(self.config.device.system_info.report());
- etx.send(Event::FoundSystemInfo(info));
+ let sysinfo = self.config.device.system_info.as_ref().expect("SystemInfo command not set");
+ etx.send(Event::FoundSystemInfo(try!(sysinfo.report())));
}
Command::SendInstalledPackages(packages) => {
@@ -218,8 +222,8 @@ impl<'t> GlobalInterpreter<'t> {
}
Command::SendSystemInfo => {
- let info = try!(self.config.device.system_info.report());
- try!(sota.send_system_info(&info));
+ let sysinfo = self.config.device.system_info.as_ref().expect("SystemInfo command not set");
+ try!(sota.send_system_info(&try!(sysinfo.report())));
etx.send(Event::SystemInfoSent);
}
diff --git a/src/main.rs b/src/main.rs
index 2caa411..fc001b7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -46,6 +46,7 @@ fn start_signal_handler(signals: Receiver<Signal>) {
}
fn start_update_poller(interval: u64, itx: Sender<Interpret>, wg: WaitGroup) {
+ info!("Polling for new updates every {} seconds.", interval);
let (etx, erx) = chan::async::<Event>();
let wait = Duration::from_secs(interval);
loop {
@@ -54,14 +55,13 @@ fn start_update_poller(interval: u64, itx: Sender<Interpret>, wg: WaitGroup) {
itx.send(Interpret {
command: Command::GetUpdateRequests,
response_tx: Some(Arc::new(Mutex::new(etx.clone())))
- });
+ }); // then request new updates
let _ = erx.recv(); // then wait for the response
}
}
fn main() {
setup_logging();
-
let config = build_config();
set_ca_certificates(Path::new(&config.device.certificates_path));
@@ -78,11 +78,6 @@ fn main() {
let signals = chan_signal::notify(&[Signal::INT, Signal::TERM]);
scope.spawn(move || start_signal_handler(signals));
- let poll_tick = config.device.polling_interval;
- let poll_itx = itx.clone();
- let poll_wg = wg.clone();
- scope.spawn(move || start_update_poller(poll_tick, poll_itx, poll_wg));
-
if config.gateway.console {
let cons_itx = itx.clone();
let cons_sub = broadcast.subscribe();
@@ -104,16 +99,21 @@ fn main() {
scope.spawn(move || http.start(http_itx, http_sub));
}
- let mut rvi = None;
- if config.gateway.rvi {
+ let rvi_services = if config.gateway.rvi {
let _ = config.dbus.as_ref().unwrap_or_else(|| exit!("{}", "dbus config required for rvi gateway"));
let rvi_cfg = config.rvi.as_ref().unwrap_or_else(|| exit!("{}", "rvi config required for rvi gateway"));
let rvi_edge = config.network.rvi_edge_server.clone();
let services = Services::new(rvi_cfg.clone(), config.device.uuid.clone(), etx.clone());
let mut edge = Edge::new(services.clone(), rvi_edge, rvi_cfg.client.clone());
scope.spawn(move || edge.start());
- rvi = Some(services);
- }
+ Some(services)
+ } else {
+ let poll_tick = config.device.polling_interval;
+ let poll_itx = itx.clone();
+ let poll_wg = wg.clone();
+ scope.spawn(move || start_update_poller(poll_tick, poll_itx, poll_wg));
+ None
+ };
if config.gateway.socket {
let socket_itx = itx.clone();
@@ -136,9 +136,11 @@ fn main() {
let event_sub = broadcast.subscribe();
let event_ctx = ctx.clone();
let event_mgr = config.device.package_manager.clone();
+ let event_sys = config.device.system_info.is_some();
let event_wg = wg.clone();
scope.spawn(move || EventInterpreter {
- pacman: event_mgr
+ pacman: event_mgr,
+ send_sysinfo: event_sys,
}.run(event_sub, event_ctx, event_wg));
let cmd_itx = itx.clone();
@@ -149,7 +151,7 @@ fn main() {
config: config,
token: None,
http_client: Box::new(AuthClient::default()),
- rvi: rvi,
+ rvi: rvi_services,
}.run(irx, etx, wg));
scope.spawn(move || broadcast.start());
@@ -259,7 +261,7 @@ fn build_config() -> Config {
config.device.polling_interval = interval.parse().unwrap_or_else(|err| exit!("Invalid device polling interval: {}", err));
});
matches.opt_str("device-certificates-path").map(|certs| config.device.certificates_path = certs);
- matches.opt_str("device-system-info").map(|cmd| config.device.system_info = SystemInfo::new(cmd));
+ matches.opt_str("device-system-info").map(|cmd| config.device.system_info = SystemInfo::new(&cmd));
matches.opt_str("gateway-console").map(|console| {
config.gateway.console = console.parse().unwrap_or_else(|err| exit!("Invalid console gateway boolean: {}", err));
diff --git a/src/sota.rs b/src/sota.rs
index 9ea615e..57f9308 100644
--- a/src/sota.rs
+++ b/src/sota.rs
@@ -1,6 +1,6 @@
use rustc_serialize::json;
+use std::{fs, io};
use std::fs::File;
-use std::io;
use std::path::PathBuf;
use datatype::{Config, DeviceReport, DownloadComplete, Error, Package,
@@ -75,6 +75,7 @@ impl<'c, 'h> Sota<'c, 'h> {
let ref pacman = self.config.device.package_manager;
let path = self.package_path(id.clone()).expect("install_update expects a valid path");
pacman.install_package(&path).and_then(|(code, output)| {
+ let _ = fs::remove_file(&path).unwrap_or_else(|err| error!("couldn't remove installed package: {}", err));
Ok(UpdateReport::single(id.clone(), code, output))
}).or_else(|(code, output)| {
Err(UpdateReport::single(id.clone(), code, output))
diff --git a/tests/sota.toml b/tests/sota.toml
index 0444a71..1c58c16 100644
--- a/tests/sota.toml
+++ b/tests/sota.toml
@@ -20,7 +20,7 @@ uuid = "123e4567-e89b-12d3-a456-426655440000"
vin = "V1234567890123456"
packages_dir = "/tmp/"
package_manager = "deb"
-system_info = "system_info.sh"
+system_info = "./system_info.sh"
polling_interval = 10
certificates_path = "/tmp/sota_certificates"
@@ -33,8 +33,8 @@ socket = false
websocket = true
[network]
-http_server = "127.0.0.1:8888"
-rvi_edge_server = "127.0.0.1:9080"
+http_server = "http://127.0.0.1:8888"
+rvi_edge_server = "http://127.0.0.1:9080"
socket_commands_path = "/tmp/sota-commands.socket"
socket_events_path = "/tmp/sota-events.socket"
websocket_server = "127.0.0.1:3012"