Merge pull request #1201 from FarmBot/qa/10.0.0

FBOS v10.0.0
staging
Rick Carlino 2020-05-19 09:34:18 -05:00 committed by GitHub
commit e897b763f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 6355 additions and 6081 deletions

View File

@ -2,7 +2,7 @@ version: 2.0
defaults: &defaults defaults: &defaults
working_directory: /nerves/build working_directory: /nerves/build
docker: docker:
- image: nervesproject/nerves_system_br:latest - image: nervesproject/nerves_system_br:1.11.3
install_elixir: &install_elixir install_elixir: &install_elixir
run: run:

View File

@ -5,6 +5,7 @@
* Deprecate `resource_update` RPC * Deprecate `resource_update` RPC
* Introduce `update_resource` RPC, which allows users to modify variables from the sequence editor. * Introduce `update_resource` RPC, which allows users to modify variables from the sequence editor.
* Genesis v1.5 and Express v1.0 firmware updates. * Genesis v1.5 and Express v1.0 firmware updates.
* Fix a bug where FBOS would not honor an "AUTO UPDATE" value of "false".
# 9.2.2 # 9.2.2

View File

@ -1 +1 @@
10.0.0-rc29 10.0.0-rc47

View File

@ -6,8 +6,8 @@ defmodule FarmbotCeleryScript.Compiler.UpdateResource do
me = unquote(__MODULE__) me = unquote(__MODULE__)
variable = unquote(Map.fetch!(args, :resource)) variable = unquote(Map.fetch!(args, :resource))
update = unquote(unpair(body, %{})) update = unquote(unpair(body, %{}))
# Go easy on the API... # Go easy on the API...
Process.sleep(1234)
case variable do case variable do
%AST{kind: :identifier} -> %AST{kind: :identifier} ->
args = Map.fetch!(variable, :args) args = Map.fetch!(variable, :args)
@ -49,7 +49,6 @@ defmodule FarmbotCeleryScript.Compiler.UpdateResource do
end end
defp unpair([pair | rest], acc) do defp unpair([pair | rest], acc) do
IO.puts("TODO: Need to apply handlebars to `value`s.")
key = Map.fetch!(pair.args, :label) key = Map.fetch!(pair.args, :label)
val = Map.fetch!(pair.args, :value) val = Map.fetch!(pair.args, :value)
next_acc = Map.merge(acc, DotProps.create(key, val)) next_acc = Map.merge(acc, DotProps.create(key, val))

View File

@ -47,8 +47,8 @@ defmodule FarmbotCeleryScript.CompilerGroupsTest do
canary_actual = :crypto.hash(:sha, Macro.to_string(result)) canary_actual = :crypto.hash(:sha, Macro.to_string(result))
canary_expected = canary_expected =
<<157, 69, 5, 38, 188, 78, 10, 183, 154, 99, 151, 193, 214, 208, 187, 130, <<136, 140, 48, 226, 216, 155, 178, 103, 244, 88, 225, 146, 130, 216, 125,
183, 73, 13, 48>> 72, 113, 195, 65, 1>>
# READ THE NOTE ABOVE IF THIS TEST FAILS!!! # READ THE NOTE ABOVE IF THIS TEST FAILS!!!
assert canary_expected == canary_actual assert canary_expected == canary_actual

View File

@ -79,7 +79,7 @@ defmodule FarmbotCeleryScript.CompilerTest do
end end
test "identifier sanitization" do test "identifier sanitization" do
label = "System.cmd(\"rm\", [\"-rf /*\"])" label = "System.cmd(\"echo\", [\"lol\"])"
value_ast = AST.Factory.new("coordinate", x: 1, y: 1, z: 1) value_ast = AST.Factory.new("coordinate", x: 1, y: 1, z: 1)
identifier_ast = AST.Factory.new("identifier", label: label) identifier_ast = AST.Factory.new("identifier", label: label)
@ -120,11 +120,19 @@ defmodule FarmbotCeleryScript.CompilerTest do
[ [
fn params -> fn params ->
_ = inspect(params) _ = inspect(params)
unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p = FarmbotCeleryScript.SysCalls.coordinate(1, 1, 1)
#{var_name} = better_params = %{
FarmbotCeleryScript.SysCalls.coordinate(1, 1, 1) "System.cmd(\\"echo\\", [\\"lol\\"])" => %FarmbotCeleryScript.AST{
args: %{x: 1, y: 1, z: 1},
body: [],
comment: nil,
kind: :coordinate,
meta: nil
}
}
[fn -> #{var_name} end] [fn -> unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p end]
end end
] ]
""") """)
@ -372,19 +380,38 @@ defmodule FarmbotCeleryScript.CompilerTest do
unsafe_cGFyZW50 = unsafe_cGFyZW50 =
Keyword.get(params, :unsafe_cGFyZW50, FarmbotCeleryScript.SysCalls.coordinate(1, 2, 3)) Keyword.get(params, :unsafe_cGFyZW50, FarmbotCeleryScript.SysCalls.coordinate(1, 2, 3))
better_params = %{}
[ [
fn -> fn ->
FarmbotCeleryScript.Compiler.UpdateResource.do_update( me = FarmbotCeleryScript.Compiler.UpdateResource
%FarmbotCeleryScript.AST{
args: %{label: "parent"}, variable = %FarmbotCeleryScript.AST{
body: [], args: %{label: "parent"},
comment: nil, body: [],
kind: :identifier, comment: nil,
meta: nil kind: :identifier,
}, meta: nil
%{"plant_stage" => "removed"}, }
[]
) update = %{"plant_stage" => "removed"}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end end
] ]
end end
@ -405,20 +432,38 @@ defmodule FarmbotCeleryScript.CompilerTest do
[ [
fn params -> fn params ->
_ = inspect(params) _ = inspect(params)
better_params = %{}
[ [
fn -> fn ->
FarmbotCeleryScript.Compiler.UpdateResource.do_update( me = FarmbotCeleryScript.Compiler.UpdateResource
%FarmbotCeleryScript.AST{
args: %{resource_id: 23, resource_type: "Plant"}, variable = %FarmbotCeleryScript.AST{
body: [], args: %{resource_id: 23, resource_type: "Plant"},
comment: nil, body: [],
kind: :resource, comment: nil,
meta: nil kind: :resource,
}, meta: nil
%{"plant_stage" => "planted", "r" => 23}, }
[]
) update = %{"plant_stage" => "planted", "r" => 23}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end end
] ]
end end

View File

@ -6,15 +6,15 @@ defmodule FarmbotCore.Asset do
""" """
alias FarmbotCore.Asset.{ alias FarmbotCore.Asset.{
Repo, CriteriaRetriever,
Device, Device,
DeviceCert, DeviceCert,
FarmwareEnv,
FirstPartyFarmware,
FarmwareInstallation,
FarmEvent, FarmEvent,
FarmwareEnv,
FarmwareInstallation,
FbosConfig, FbosConfig,
FirmwareConfig, FirmwareConfig,
FirstPartyFarmware,
Peripheral, Peripheral,
PinBinding, PinBinding,
Point, Point,
@ -22,11 +22,11 @@ defmodule FarmbotCore.Asset do
PublicKey, PublicKey,
Regimen, Regimen,
RegimenInstance, RegimenInstance,
Sequence, Repo,
Sensor, Sensor,
SensorReading, SensorReading,
Sequence,
Tool, Tool,
CriteriaRetriever
} }
alias FarmbotCore.AssetSupervisor alias FarmbotCore.AssetSupervisor
@ -251,7 +251,7 @@ defmodule FarmbotCore.Asset do
end end
def update_point(point, params) do def update_point(point, params) do
# TODO: RC 8 MAY 20202 - We need to hard refresh the point. # TODO: RC 8 MAY 2020 - We need to hard refresh the point.
# The CSVM appears to be caching resources. This leads # The CSVM appears to be caching resources. This leads
# to problems when a user runs a sequence that has two # to problems when a user runs a sequence that has two
# MARK AS steps. # MARK AS steps.

View File

@ -45,9 +45,7 @@ defmodule FarmbotCore.Asset.Private do
# error being thrown. # error being thrown.
changeset = LocalMeta.changeset(local_meta, Map.merge(params, %{table: table, status: "dirty"})) changeset = LocalMeta.changeset(local_meta, Map.merge(params, %{table: table, status: "dirty"}))
try do try do
result = Repo.insert_or_update!(changeset) Repo.insert_or_update!(changeset)
%FarmbotCore.Asset.Private.LocalMeta{} = result
result
catch catch
:error, %Sqlite.DbConnection.Error{ :error, %Sqlite.DbConnection.Error{
message: "UNIQUE constraint failed: local_metas.table, local_metas.asset_local_id", message: "UNIQUE constraint failed: local_metas.table, local_metas.asset_local_id",

View File

@ -6,8 +6,9 @@ defmodule FarmbotExt.API.DirtyWorker do
import API.View, only: [render: 2] import API.View, only: [render: 2]
require Logger require Logger
require FarmbotCore.Logger
use GenServer use GenServer
@timeout 4700 @timeout 500
# these resources can't be accessed by `id`. # these resources can't be accessed by `id`.
@singular [ @singular [
@ -35,66 +36,68 @@ defmodule FarmbotExt.API.DirtyWorker do
@impl GenServer @impl GenServer
def init(args) do def init(args) do
module = Keyword.fetch!(args, :module) module = Keyword.fetch!(args, :module)
timeout = Keyword.get(args, :timeout, @timeout) Process.send_after(self(), :do_work, @timeout)
timer = Process.send_after(self(), :timeout, timeout) {:ok, %{module: module}}
{:ok, %{module: module, timeout: timeout, timer: timer}}
end end
@impl GenServer @impl GenServer
def handle_info(:timeout, %{module: module} = state) do def handle_info(:do_work, %{module: module} = state) do
dirty = Private.list_dirty(module) Process.sleep(@timeout)
local = Private.list_local(module) list = Enum.uniq(Private.list_dirty(module) ++ Private.list_local(module))
{:noreply, state, {:continue, Enum.uniq(dirty ++ local)}}
unless has_race_condition?(module, list) do
Enum.map(list, fn dirty -> work(dirty, module) end)
end
Process.send_after(self(), :do_work, @timeout)
{:noreply, state}
end end
@impl GenServer def work(dirty, module) do
def handle_continue([], state) do # Go easy on the API
timer = Process.send_after(self(), :timeout, state.timeout) Process.sleep(333)
{:noreply, %{state | timer: timer}}
end
def handle_continue([dirty | rest], %{module: module} = state) do case http_request(dirty, module) do
case http_request(dirty, state) do
# Valid data # Valid data
{:ok, %{status: s, body: body}} when s > 199 and s < 300 -> {:ok, %{status: s, body: body}} when s > 199 and s < 300 ->
dirty |> module.changeset(body) |> handle_changeset(rest, state) dirty |> module.changeset(body) |> handle_changeset(module)
# Invalid data # Invalid data
{:ok, %{status: s, body: %{} = body}} when s > 399 and s < 500 -> {:ok, %{status: s, body: %{} = body}} when s > 399 and s < 500 ->
FarmbotCore.Logger.error(2, "HTTP Error #{s}. #{inspect(body)}")
changeset = module.changeset(dirty) changeset = module.changeset(dirty)
Enum.reduce(body, changeset, fn {key, val}, changeset -> Enum.reduce(body, changeset, fn {key, val}, changeset ->
Ecto.Changeset.add_error(changeset, key, val) Ecto.Changeset.add_error(changeset, key, val)
end) end)
|> handle_changeset(rest, state) |> handle_changeset(module)
# Invalid data, but the API didn't say why # Invalid data, but the API didn't say why
{:ok, %{status: s, body: _body}} when s > 399 and s < 500 -> {:ok, %{status: s, body: _body}} when s > 399 and s < 500 ->
FarmbotCore.Logger.error(2, "HTTP Error #{s}. #{inspect(dirty)}")
module.changeset(dirty) module.changeset(dirty)
|> Map.put(:valid?, false) |> Map.put(:valid?, false)
|> handle_changeset(rest, state) |> handle_changeset(module)
# HTTP Error. (500, network error, timeout etc.) # HTTP Error. (500, network error, timeout etc.)
error -> error ->
Logger.error( FarmbotCore.Logger.error(
"[#{module} #{dirty.local_id} #{inspect(self())}] HTTP Error: #{state.module} #{ 2,
"[#{module} #{dirty.local_id} #{inspect(self())}] HTTP Error: #{module} #{
inspect(error) inspect(error)
}" }"
) )
{:noreply, state, @timeout}
end end
end end
# If the changeset was valid, update the record. # If the changeset was valid, update the record.
def handle_changeset(%{valid?: true} = changeset, rest, state) do def handle_changeset(%{valid?: true} = changeset, _module) do
Repo.update!(changeset) Private.mark_clean!(Repo.update!(changeset))
|> Private.mark_clean!() :ok
{:noreply, state, {:continue, rest}}
end end
def handle_changeset(%{valid?: false, data: data} = changeset, rest, state) do def handle_changeset(%{valid?: false, data: data} = changeset, module) do
message = message =
Enum.map(changeset.errors, fn Enum.map(changeset.errors, fn
{key, {msg, _meta}} when is_binary(key) -> "\t#{key}: #{msg}" {key, {msg, _meta}} when is_binary(key) -> "\t#{key}: #{msg}"
@ -102,26 +105,64 @@ defmodule FarmbotExt.API.DirtyWorker do
end) end)
|> Enum.join("\n") |> Enum.join("\n")
Logger.error("Failed to sync: #{state.module} \n #{message}") FarmbotCore.Logger.error(3, "Failed to sync: #{module} \n #{message}")
_ = Repo.delete!(data) _ = Repo.delete!(data)
{:noreply, state, {:continue, rest}} :ok
end end
defp http_request(%{id: nil} = dirty, state) do defp http_request(%{id: nil} = dirty, module) do
path = state.module.path() path = module.path()
data = render(state.module, dirty) data = render(module, dirty)
API.post(API.client(), path, data) API.post(API.client(), path, data)
end end
defp http_request(dirty, %{module: module} = state) when module in @singular do defp http_request(dirty, module) when module in @singular do
path = path = state.module.path() path = path = module.path()
data = render(state.module, dirty) data = render(module, dirty)
API.patch(API.client(), path, data) API.patch(API.client(), path, data)
end end
defp http_request(dirty, state) do defp http_request(dirty, module) do
path = Path.join(state.module.path(), to_string(dirty.id)) path = Path.join(module.path(), to_string(dirty.id))
data = render(state.module, dirty) data = render(module, dirty)
API.patch(API.client(), path, data) API.patch(API.client(), path, data)
end end
# This is a fix for a race condtion. The root cause is unknown
# as of 18 May 2020. The problem is that records are marked
# diry _before_ the dirty data is saved. That means that FBOS
# knows a record has changed, but for a brief moment, it only
# has the old copy of the record (not the changes).
# Because of this race condtion,
# The race condition:
#
# * Is nondeterministic
# * Happens frequently when running many MARK AS steps in one go.
# * Happens frequently when Erlang VM only has one thread
# * Ie: `iex --erl '+S 1 +A 1' -S mix`
# * Happens frequently when @timeout is decreased to `1`.
#
# This function PREVENTS CORRUPTION OF API DATA. It can be
# removed once the root cause of the data race is determined.
# - RC 18 May 2020
def has_race_condition?(module, list) do
Enum.find(list, fn item ->
if item.id do
if item == Repo.get_by(module, id: item.id) do
# This item is OK - no race condition.
false
else
# There was a race condtion. We probably can't trust
# any of the data in this list. We need to wait and
# try again later.
Process.sleep(@timeout * 3)
true
end
else
# This item only exists on the FBOS side.
# It will never be affected by the data race condtion.
false
end
end)
end
end end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -32,11 +32,11 @@ defmodule FarmbotOS.SysCalls.ResourceUpdate do
"Weed" => "weed" "Weed" => "weed"
} }
def notify_user_of_updates(kind, params) do def notify_user_of_updates(kind, params, id \\ nil) do
Enum.map(params, fn {k, v} -> Enum.map(params, fn {k, v} ->
name = @friendly_names[kind] || kind name = @friendly_names[kind] || kind
property = @friendly_names["#{k}"] || k property = @friendly_names["#{k}"] || k
msg = "Setting #{name} #{property} to #{inspect(v)}" msg = "Setting #{name} #{id} #{property} to #{inspect(v)}"
FarmbotCore.Logger.info(3, msg) FarmbotCore.Logger.info(3, msg)
end) end)
end end
@ -53,7 +53,7 @@ defmodule FarmbotOS.SysCalls.ResourceUpdate do
end end
def update_resource(kind, id, params) when kind in @point_kinds do def update_resource(kind, id, params) when kind in @point_kinds do
notify_user_of_updates(kind, params) notify_user_of_updates(kind, params, id)
params = do_handlebars(params) params = do_handlebars(params)
point_update_resource(kind, id, params) point_update_resource(kind, id, params)
end end

View File

@ -542,7 +542,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
"ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}" "ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}"
) )
true !!auto_update
end end
result && !currently_downloading?() result && !currently_downloading?()